guohanghui commited on
Commit
05229b8
·
verified ·
1 Parent(s): 57fdba3

Update backtrader/mcp_output/mcp_plugin/mcp_service.py

Browse files
backtrader/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -6,297 +6,606 @@ if source_path not in sys.path:
6
  sys.path.insert(0, source_path)
7
 
8
  from fastmcp import FastMCP
 
 
9
 
10
- from contrib.samples.pair-trading import parse_args, runstrategy, PairTradingStrategy
11
- from contrib.utils.influxdb-import import InfluxDBTool
12
- from contrib.utils.iqfeed-to-influxdb import IQFeedTool
13
- from samples.analyzer-annualreturn import parse_args, LongShortStrategy, runstrategy
14
- from samples.bidask-to-ohlc import runstrat, parse_args, St
15
- from samples.bracket import parse_args
16
 
17
- mcp = FastMCP("unknown_service")
 
18
 
 
 
 
19
 
20
- @mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
21
- def parse_args(payload: dict):
22
- try:
23
- if parse_args is None:
24
- return {"success": False, "result": None, "error": "Function parse_args is not available"}
25
- result = parse_args(**payload)
26
- return {"success": True, "result": result, "error": None}
27
- except Exception as e:
28
- return {"success": False, "result": None, "error": str(e)}
 
 
 
 
 
 
29
 
30
- @mcp.tool(name="runstrategy", description="Auto-wrapped function runstrategy")
31
- def runstrategy(payload: dict):
 
 
 
 
32
  try:
33
- if runstrategy is None:
34
- return {"success": False, "result": None, "error": "Function runstrategy is not available"}
35
- result = runstrategy(**payload)
36
- return {"success": True, "result": result, "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  except Exception as e:
38
- return {"success": False, "result": None, "error": str(e)}
 
39
 
40
- @mcp.tool(name="pairtradingstrategy", description="PairTradingStrategy class")
41
- def pairtradingstrategy(*args, **kwargs):
42
- """PairTradingStrategy class"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  try:
44
- if PairTradingStrategy is None:
45
- return {"success": False, "result": None, "error": "Class PairTradingStrategy is not available, path may need adjustment"}
46
 
47
- # MCP parameter type conversion
48
- converted_args = []
49
- converted_kwargs = kwargs.copy()
 
 
 
 
50
 
51
- # Handle position argument type conversion
52
- for arg in args:
53
- if isinstance(arg, str):
54
- # Try to convert to numeric type
55
- try:
56
- if '.' in arg:
57
- converted_args.append(float(arg))
58
- else:
59
- converted_args.append(int(arg))
60
- except ValueError:
61
- converted_args.append(arg)
62
- else:
63
- converted_args.append(arg)
 
 
 
 
 
 
 
 
 
 
64
 
65
- # Handle keyword argument type conversion
66
- for key, value in converted_kwargs.items():
67
- if isinstance(value, str):
68
- try:
69
- if '.' in value:
70
- converted_kwargs[key] = float(value)
71
- else:
72
- converted_kwargs[key] = int(value)
73
- except ValueError:
74
- pass
75
 
76
- instance = PairTradingStrategy(*converted_args, **converted_kwargs)
77
- return {"success": True, "result": str(instance), "error": None}
 
 
 
 
 
 
78
  except Exception as e:
79
- return {"success": False, "result": None, "error": str(e)}
 
 
 
 
 
 
80
 
81
- @mcp.tool(name="influxdbtool", description="InfluxDBTool class")
82
- def influxdbtool(*args, **kwargs):
83
- """InfluxDBTool class"""
 
 
 
 
 
 
 
 
 
 
 
84
  try:
85
- if InfluxDBTool is None:
86
- return {"success": False, "result": None, "error": "Class InfluxDBTool is not available, path may need adjustment"}
87
-
88
- # MCP parameter type conversion
89
- converted_args = []
90
- converted_kwargs = kwargs.copy()
91
 
92
- # Handle position argument type conversion
93
- for arg in args:
94
- if isinstance(arg, str):
95
- # Try to convert to numeric type
96
- try:
97
- if '.' in arg:
98
- converted_args.append(float(arg))
99
- else:
100
- converted_args.append(int(arg))
101
- except ValueError:
102
- converted_args.append(arg)
103
- else:
104
- converted_args.append(arg)
 
 
 
 
 
 
 
105
 
106
- # Handle keyword argument type conversion
107
- for key, value in converted_kwargs.items():
108
- if isinstance(value, str):
109
- try:
110
- if '.' in value:
111
- converted_kwargs[key] = float(value)
112
- else:
113
- converted_kwargs[key] = int(value)
114
- except ValueError:
115
- pass
116
 
117
- instance = InfluxDBTool(*converted_args, **converted_kwargs)
118
- return {"success": True, "result": str(instance), "error": None}
 
 
 
 
 
119
  except Exception as e:
120
- return {"success": False, "result": None, "error": str(e)}
 
121
 
122
- @mcp.tool(name="iqfeedtool", description="IQFeedTool class")
123
- def iqfeedtool(*args, **kwargs):
124
- """IQFeedTool class"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  try:
126
- if IQFeedTool is None:
127
- return {"success": False, "result": None, "error": "Class IQFeedTool is not available, path may need adjustment"}
128
 
129
- # MCP parameter type conversion
130
- converted_args = []
131
- converted_kwargs = kwargs.copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- # Handle position argument type conversion
134
- for arg in args:
135
- if isinstance(arg, str):
136
- # Try to convert to numeric type
137
- try:
138
- if '.' in arg:
139
- converted_args.append(float(arg))
140
- else:
141
- converted_args.append(int(arg))
142
- except ValueError:
143
- converted_args.append(arg)
144
- else:
145
- converted_args.append(arg)
146
 
147
- # Handle keyword argument type conversion
148
- for key, value in converted_kwargs.items():
149
- if isinstance(value, str):
150
- try:
151
- if '.' in value:
152
- converted_kwargs[key] = float(value)
153
- else:
154
- converted_kwargs[key] = int(value)
155
- except ValueError:
156
- pass
157
-
158
- instance = IQFeedTool(*converted_args, **converted_kwargs)
159
- return {"success": True, "result": str(instance), "error": None}
160
  except Exception as e:
161
- return {"success": False, "result": None, "error": str(e)}
 
162
 
163
- @mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
164
- def parse_args(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  try:
166
- if parse_args is None:
167
- return {"success": False, "result": None, "error": "Function parse_args is not available"}
168
- result = parse_args(**payload)
169
- return {"success": True, "result": result, "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  except Exception as e:
171
- return {"success": False, "result": None, "error": str(e)}
 
 
 
 
 
 
172
 
173
- @mcp.tool(name="runstrategy", description="Auto-wrapped function runstrategy")
174
- def runstrategy(payload: dict):
 
 
 
 
 
 
 
 
 
 
175
  try:
176
- if runstrategy is None:
177
- return {"success": False, "result": None, "error": "Function runstrategy is not available"}
178
- result = runstrategy(**payload)
179
- return {"success": True, "result": result, "error": None}
 
 
 
 
 
 
 
 
180
  except Exception as e:
181
- return {"success": False, "result": None, "error": str(e)}
182
 
183
- @mcp.tool(name="longshortstrategy", description="LongShortStrategy class")
184
- def longshortstrategy(*args, **kwargs):
185
- """LongShortStrategy class"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  try:
187
- if LongShortStrategy is None:
188
- return {"success": False, "result": None, "error": "Class LongShortStrategy is not available, path may need adjustment"}
189
 
190
- # MCP parameter type conversion
191
- converted_args = []
192
- converted_kwargs = kwargs.copy()
193
 
194
- # Handle position argument type conversion
195
- for arg in args:
196
- if isinstance(arg, str):
197
- # Try to convert to numeric type
198
- try:
199
- if '.' in arg:
200
- converted_args.append(float(arg))
201
- else:
202
- converted_args.append(int(arg))
203
- except ValueError:
204
- converted_args.append(arg)
205
- else:
206
- converted_args.append(arg)
207
 
208
- # Handle keyword argument type conversion
209
- for key, value in converted_kwargs.items():
210
- if isinstance(value, str):
211
- try:
212
- if '.' in value:
213
- converted_kwargs[key] = float(value)
214
- else:
215
- converted_kwargs[key] = int(value)
216
- except ValueError:
217
- pass
218
 
219
- instance = LongShortStrategy(*converted_args, **converted_kwargs)
220
- return {"success": True, "result": str(instance), "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  except Exception as e:
222
- return {"success": False, "result": None, "error": str(e)}
 
 
 
 
 
 
223
 
224
- @mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
225
- def parse_args(payload: dict):
 
 
 
 
 
 
 
 
 
 
226
  try:
227
- if parse_args is None:
228
- return {"success": False, "result": None, "error": "Function parse_args is not available"}
229
- result = parse_args(**payload)
230
- return {"success": True, "result": result, "error": None}
 
 
 
 
 
 
 
231
  except Exception as e:
232
- return {"success": False, "result": None, "error": str(e)}
 
 
 
 
 
233
 
234
- @mcp.tool(name="runstrat", description="Auto-wrapped function runstrat")
235
- def runstrat(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  try:
237
- if runstrat is None:
238
- return {"success": False, "result": None, "error": "Function runstrat is not available"}
239
- result = runstrat(**payload)
240
- return {"success": True, "result": result, "error": None}
 
 
 
 
 
 
 
241
  except Exception as e:
242
- return {"success": False, "result": None, "error": str(e)}
243
 
244
- @mcp.tool(name="st", description="St class")
245
- def st(*args, **kwargs):
246
- """St class"""
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  try:
248
- if St is None:
249
- return {"success": False, "result": None, "error": "Class St is not available, path may need adjustment"}
250
-
251
- # MCP parameter type conversion
252
- converted_args = []
253
- converted_kwargs = kwargs.copy()
254
 
255
- # Handle position argument type conversion
256
- for arg in args:
257
- if isinstance(arg, str):
258
- # Try to convert to numeric type
259
- try:
260
- if '.' in arg:
261
- converted_args.append(float(arg))
262
- else:
263
- converted_args.append(int(arg))
264
- except ValueError:
265
- converted_args.append(arg)
266
- else:
267
- converted_args.append(arg)
268
-
269
- # Handle keyword argument type conversion
270
- for key, value in converted_kwargs.items():
271
- if isinstance(value, str):
272
- try:
273
- if '.' in value:
274
- converted_kwargs[key] = float(value)
275
- else:
276
- converted_kwargs[key] = int(value)
277
- except ValueError:
278
- pass
279
-
280
- instance = St(*converted_args, **converted_kwargs)
281
- return {"success": True, "result": str(instance), "error": None}
 
 
282
  except Exception as e:
283
- return {"success": False, "result": None, "error": str(e)}
 
 
 
 
 
 
284
 
285
- @mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
286
- def parse_args(payload: dict):
 
 
 
 
 
 
 
 
287
  try:
288
- if parse_args is None:
289
- return {"success": False, "result": None, "error": "Function parse_args is not available"}
290
- result = parse_args(**payload)
291
- return {"success": True, "result": result, "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  except Exception as e:
293
- return {"success": False, "result": None, "error": str(e)}
294
 
295
 
 
 
 
296
 
297
- def create_app():
298
- """Create and return FastMCP application instance"""
 
299
  return mcp
300
 
301
  if __name__ == "__main__":
302
- mcp.run(transport="http", host="0.0.0.0", port=8000)
 
6
  sys.path.insert(0, source_path)
7
 
8
  from fastmcp import FastMCP
9
+ import json
10
+ import datetime
11
 
12
+ # Check if backtrader is available
13
+ try:
14
+ import backtrader as bt
15
+ BACKTRADER_AVAILABLE = True
16
+ except ImportError:
17
+ BACKTRADER_AVAILABLE = False
18
 
19
+ # Create the FastMCP service application
20
+ mcp = FastMCP("backtrader_service")
21
 
22
+ # Store cerebro instances in memory
23
+ _cerebros = {}
24
+ _cerebro_counter = 0
25
 
26
+ # ============================================================================
27
+ # Cerebro (Backtesting Engine) Creation Tools
28
+ # ============================================================================
29
+
30
+ @mcp.tool()
31
+ def create_cerebro(initial_cash: float = 10000.0, commission: float = 0.001,
32
+ preload: bool = True, runonce: bool = True) -> dict:
33
+ """
34
+ Create a new Cerebro backtesting engine instance.
35
+
36
+ Parameters:
37
+ - initial_cash: Initial portfolio cash (default 10000)
38
+ - commission: Commission rate (default 0.001 = 0.1%)
39
+ - preload: Preload data feeds (default True)
40
+ - runonce: Run in vectorized mode (default True)
41
 
42
+ Returns:
43
+ - dict: Cerebro ID and configuration
44
+ """
45
+ if not BACKTRADER_AVAILABLE:
46
+ return {"success": False, "error": "backtrader not installed"}
47
+
48
  try:
49
+ global _cerebro_counter
50
+
51
+ # Create cerebro instance
52
+ cerebro = bt.Cerebro(preload=preload, runonce=runonce)
53
+ cerebro.broker.setcash(initial_cash)
54
+ cerebro.broker.setcommission(commission=commission)
55
+
56
+ cerebro_id = f"cerebro_{_cerebro_counter}"
57
+ _cerebros[cerebro_id] = cerebro
58
+ _cerebro_counter += 1
59
+
60
+ return {
61
+ "success": True,
62
+ "cerebro_id": cerebro_id,
63
+ "initial_cash": initial_cash,
64
+ "commission": commission,
65
+ "preload": preload,
66
+ "runonce": runonce
67
+ }
68
  except Exception as e:
69
+ return {"success": False, "error": str(e)}
70
+
71
 
72
+ @mcp.tool()
73
+ def add_data_feed(cerebro_id: str, data_source: str, name: str = "data",
74
+ fromdate: str = None, todate: str = None) -> dict:
75
+ """
76
+ Add a data feed to Cerebro (CSV file path or built-in data).
77
+
78
+ Parameters:
79
+ - cerebro_id: Cerebro instance ID
80
+ - data_source: Path to CSV file or 'sample' for built-in data
81
+ - name: Name for the data feed
82
+ - fromdate: Start date (YYYY-MM-DD format)
83
+ - todate: End date (YYYY-MM-DD format)
84
+
85
+ Returns:
86
+ - dict: Data feed info
87
+ """
88
+ if not BACKTRADER_AVAILABLE:
89
+ return {"success": False, "error": "backtrader not installed"}
90
+
91
+ if cerebro_id not in _cerebros:
92
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
93
+
94
  try:
95
+ cerebro = _cerebros[cerebro_id]
 
96
 
97
+ # Parse dates if provided
98
+ from_dt = None
99
+ to_dt = None
100
+ if fromdate:
101
+ from_dt = datetime.datetime.strptime(fromdate, "%Y-%m-%d")
102
+ if todate:
103
+ to_dt = datetime.datetime.strptime(todate, "%Y-%m-%d")
104
 
105
+ # Create data feed
106
+ if data_source == "sample":
107
+ # Use sample data if available
108
+ data = bt.feeds.GenericCSVData(
109
+ dataname=None, # Would need actual sample data path
110
+ name=name
111
+ )
112
+ else:
113
+ # Load from CSV file
114
+ data = bt.feeds.GenericCSVData(
115
+ dataname=data_source,
116
+ fromdate=from_dt,
117
+ todate=to_dt,
118
+ name=name,
119
+ dtformat='%Y-%m-%d',
120
+ datetime=0,
121
+ open=1,
122
+ high=2,
123
+ low=3,
124
+ close=4,
125
+ volume=5,
126
+ openinterest=-1
127
+ )
128
 
129
+ cerebro.adddata(data)
 
 
 
 
 
 
 
 
 
130
 
131
+ return {
132
+ "success": True,
133
+ "cerebro_id": cerebro_id,
134
+ "data_name": name,
135
+ "data_source": data_source,
136
+ "fromdate": fromdate,
137
+ "todate": todate
138
+ }
139
  except Exception as e:
140
+ return {"success": False, "error": str(e)}
141
+
142
+
143
+ @mcp.tool()
144
+ def add_sma_crossover_strategy(cerebro_id: str, fast_period: int = 10, slow_period: int = 30) -> dict:
145
+ """
146
+ Add a simple SMA crossover strategy to Cerebro.
147
 
148
+ Parameters:
149
+ - cerebro_id: Cerebro instance ID
150
+ - fast_period: Fast SMA period (default 10)
151
+ - slow_period: Slow SMA period (default 30)
152
+
153
+ Returns:
154
+ - dict: Strategy info
155
+ """
156
+ if not BACKTRADER_AVAILABLE:
157
+ return {"success": False, "error": "backtrader not installed"}
158
+
159
+ if cerebro_id not in _cerebros:
160
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
161
+
162
  try:
163
+ cerebro = _cerebros[cerebro_id]
 
 
 
 
 
164
 
165
+ # Define SMA crossover strategy
166
+ class SMACrossover(bt.Strategy):
167
+ params = (
168
+ ('fast_period', fast_period),
169
+ ('slow_period', slow_period),
170
+ )
171
+
172
+ def __init__(self):
173
+ self.fast_sma = bt.indicators.SimpleMovingAverage(
174
+ self.data.close, period=self.params.fast_period)
175
+ self.slow_sma = bt.indicators.SimpleMovingAverage(
176
+ self.data.close, period=self.params.slow_period)
177
+ self.crossover = bt.indicators.CrossOver(self.fast_sma, self.slow_sma)
178
+
179
+ def next(self):
180
+ if not self.position:
181
+ if self.crossover > 0: # Fast crosses above slow
182
+ self.buy()
183
+ elif self.crossover < 0: # Fast crosses below slow
184
+ self.close()
185
 
186
+ cerebro.addstrategy(SMACrossover, fast_period=fast_period, slow_period=slow_period)
 
 
 
 
 
 
 
 
 
187
 
188
+ return {
189
+ "success": True,
190
+ "cerebro_id": cerebro_id,
191
+ "strategy": "SMACrossover",
192
+ "fast_period": fast_period,
193
+ "slow_period": slow_period
194
+ }
195
  except Exception as e:
196
+ return {"success": False, "error": str(e)}
197
+
198
 
199
+ @mcp.tool()
200
+ def add_rsi_strategy(cerebro_id: str, rsi_period: int = 14,
201
+ oversold: int = 30, overbought: int = 70) -> dict:
202
+ """
203
+ Add an RSI-based strategy to Cerebro.
204
+
205
+ Parameters:
206
+ - cerebro_id: Cerebro instance ID
207
+ - rsi_period: RSI period (default 14)
208
+ - oversold: Oversold threshold (default 30)
209
+ - overbought: Overbought threshold (default 70)
210
+
211
+ Returns:
212
+ - dict: Strategy info
213
+ """
214
+ if not BACKTRADER_AVAILABLE:
215
+ return {"success": False, "error": "backtrader not installed"}
216
+
217
+ if cerebro_id not in _cerebros:
218
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
219
+
220
  try:
221
+ cerebro = _cerebros[cerebro_id]
 
222
 
223
+ # Define RSI strategy
224
+ class RSIStrategy(bt.Strategy):
225
+ params = (
226
+ ('rsi_period', rsi_period),
227
+ ('oversold', oversold),
228
+ ('overbought', overbought),
229
+ )
230
+
231
+ def __init__(self):
232
+ self.rsi = bt.indicators.RSI(
233
+ self.data.close,
234
+ period=self.params.rsi_period
235
+ )
236
+
237
+ def next(self):
238
+ if not self.position:
239
+ if self.rsi < self.params.oversold:
240
+ self.buy()
241
+ elif self.rsi > self.params.overbought:
242
+ self.close()
243
 
244
+ cerebro.addstrategy(RSIStrategy, rsi_period=rsi_period,
245
+ oversold=oversold, overbought=overbought)
 
 
 
 
 
 
 
 
 
 
 
246
 
247
+ return {
248
+ "success": True,
249
+ "cerebro_id": cerebro_id,
250
+ "strategy": "RSIStrategy",
251
+ "rsi_period": rsi_period,
252
+ "oversold": oversold,
253
+ "overbought": overbought
254
+ }
 
 
 
 
 
255
  except Exception as e:
256
+ return {"success": False, "error": str(e)}
257
+
258
 
259
+ # ============================================================================
260
+ # Analysis and Observation Tools
261
+ # ============================================================================
262
+
263
+ @mcp.tool()
264
+ def add_analyzers(cerebro_id: str, analyzers: list = None) -> dict:
265
+ """
266
+ Add analyzers to track strategy performance.
267
+
268
+ Parameters:
269
+ - cerebro_id: Cerebro instance ID
270
+ - analyzers: List of analyzer names (e.g., ['SharpeRatio', 'Returns', 'DrawDown'])
271
+
272
+ Returns:
273
+ - dict: Added analyzers
274
+ """
275
+ if not BACKTRADER_AVAILABLE:
276
+ return {"success": False, "error": "backtrader not installed"}
277
+
278
+ if cerebro_id not in _cerebros:
279
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
280
+
281
  try:
282
+ cerebro = _cerebros[cerebro_id]
283
+
284
+ if analyzers is None:
285
+ analyzers = ['SharpeRatio', 'Returns', 'DrawDown', 'TradeAnalyzer']
286
+
287
+ added = []
288
+ for analyzer_name in analyzers:
289
+ if analyzer_name == 'SharpeRatio':
290
+ cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
291
+ added.append('SharpeRatio')
292
+ elif analyzer_name == 'Returns':
293
+ cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
294
+ added.append('Returns')
295
+ elif analyzer_name == 'DrawDown':
296
+ cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
297
+ added.append('DrawDown')
298
+ elif analyzer_name == 'TradeAnalyzer':
299
+ cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
300
+ added.append('TradeAnalyzer')
301
+
302
+ return {
303
+ "success": True,
304
+ "cerebro_id": cerebro_id,
305
+ "analyzers": added
306
+ }
307
  except Exception as e:
308
+ return {"success": False, "error": str(e)}
309
+
310
+
311
+ @mcp.tool()
312
+ def add_observers(cerebro_id: str) -> dict:
313
+ """
314
+ Add standard observers (Broker, Trades, BuySell).
315
 
316
+ Parameters:
317
+ - cerebro_id: Cerebro instance ID
318
+
319
+ Returns:
320
+ - dict: Observers info
321
+ """
322
+ if not BACKTRADER_AVAILABLE:
323
+ return {"success": False, "error": "backtrader not installed"}
324
+
325
+ if cerebro_id not in _cerebros:
326
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
327
+
328
  try:
329
+ cerebro = _cerebros[cerebro_id]
330
+
331
+ # Standard observers are added by default, but can be explicitly added
332
+ cerebro.addobserver(bt.observers.Broker)
333
+ cerebro.addobserver(bt.observers.Trades)
334
+ cerebro.addobserver(bt.observers.BuySell)
335
+
336
+ return {
337
+ "success": True,
338
+ "cerebro_id": cerebro_id,
339
+ "observers": ["Broker", "Trades", "BuySell"]
340
+ }
341
  except Exception as e:
342
+ return {"success": False, "error": str(e)}
343
 
344
+
345
+ # ============================================================================
346
+ # Backtesting Execution Tools
347
+ # ============================================================================
348
+
349
+ @mcp.tool()
350
+ def run_backtest(cerebro_id: str) -> dict:
351
+ """
352
+ Run the backtest and return results.
353
+
354
+ Parameters:
355
+ - cerebro_id: Cerebro instance ID
356
+
357
+ Returns:
358
+ - dict: Backtest results
359
+ """
360
+ if not BACKTRADER_AVAILABLE:
361
+ return {"success": False, "error": "backtrader not installed"}
362
+
363
+ if cerebro_id not in _cerebros:
364
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
365
+
366
  try:
367
+ cerebro = _cerebros[cerebro_id]
 
368
 
369
+ # Get starting values
370
+ starting_value = cerebro.broker.getvalue()
 
371
 
372
+ # Run backtest
373
+ results = cerebro.run()
 
 
 
 
 
 
 
 
 
 
 
374
 
375
+ # Get ending values
376
+ ending_value = cerebro.broker.getvalue()
 
 
 
 
 
 
 
 
377
 
378
+ # Extract analyzer results
379
+ strategy = results[0]
380
+ analyzer_results = {}
381
+
382
+ if hasattr(strategy, 'analyzers'):
383
+ if hasattr(strategy.analyzers, 'sharpe'):
384
+ sharpe = strategy.analyzers.sharpe.get_analysis()
385
+ analyzer_results['sharpe_ratio'] = sharpe.get('sharperatio', None)
386
+
387
+ if hasattr(strategy.analyzers, 'returns'):
388
+ returns = strategy.analyzers.returns.get_analysis()
389
+ analyzer_results['total_return'] = returns.get('rtot', None)
390
+
391
+ if hasattr(strategy.analyzers, 'drawdown'):
392
+ dd = strategy.analyzers.drawdown.get_analysis()
393
+ analyzer_results['max_drawdown'] = dd.get('max', {}).get('drawdown', None)
394
+
395
+ if hasattr(strategy.analyzers, 'trades'):
396
+ trades = strategy.analyzers.trades.get_analysis()
397
+ analyzer_results['total_trades'] = trades.get('total', {}).get('total', 0)
398
+ analyzer_results['won_trades'] = trades.get('won', {}).get('total', 0)
399
+ analyzer_results['lost_trades'] = trades.get('lost', {}).get('total', 0)
400
+
401
+ return {
402
+ "success": True,
403
+ "cerebro_id": cerebro_id,
404
+ "starting_value": starting_value,
405
+ "ending_value": ending_value,
406
+ "profit": ending_value - starting_value,
407
+ "return_pct": ((ending_value - starting_value) / starting_value) * 100,
408
+ "analyzers": analyzer_results
409
+ }
410
  except Exception as e:
411
+ return {"success": False, "error": str(e)}
412
+
413
+
414
+ @mcp.tool()
415
+ def get_portfolio_value(cerebro_id: str) -> dict:
416
+ """
417
+ Get current portfolio value.
418
 
419
+ Parameters:
420
+ - cerebro_id: Cerebro instance ID
421
+
422
+ Returns:
423
+ - dict: Portfolio value
424
+ """
425
+ if not BACKTRADER_AVAILABLE:
426
+ return {"success": False, "error": "backtrader not installed"}
427
+
428
+ if cerebro_id not in _cerebros:
429
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
430
+
431
  try:
432
+ cerebro = _cerebros[cerebro_id]
433
+ value = cerebro.broker.getvalue()
434
+ cash = cerebro.broker.getcash()
435
+
436
+ return {
437
+ "success": True,
438
+ "cerebro_id": cerebro_id,
439
+ "portfolio_value": value,
440
+ "cash": cash,
441
+ "invested": value - cash
442
+ }
443
  except Exception as e:
444
+ return {"success": False, "error": str(e)}
445
+
446
+
447
+ # ============================================================================
448
+ # Optimization Tools
449
+ # ============================================================================
450
 
451
+ @mcp.tool()
452
+ def optimize_strategy(cerebro_id: str, param_name: str,
453
+ start: int, end: int, step: int = 1) -> dict:
454
+ """
455
+ Optimize a strategy parameter (NOTE: Creates new cerebro for optimization).
456
+
457
+ Parameters:
458
+ - cerebro_id: Base cerebro instance ID
459
+ - param_name: Parameter name to optimize (e.g., 'fast_period')
460
+ - start: Start value
461
+ - end: End value
462
+ - step: Step size
463
+
464
+ Returns:
465
+ - dict: Optimization results
466
+ """
467
+ if not BACKTRADER_AVAILABLE:
468
+ return {"success": False, "error": "backtrader not installed"}
469
+
470
+ if cerebro_id not in _cerebros:
471
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
472
+
473
  try:
474
+ # Note: This is a simplified version
475
+ # Real optimization would need to recreate cerebro with optstrategy
476
+
477
+ return {
478
+ "success": True,
479
+ "cerebro_id": cerebro_id,
480
+ "message": "Optimization setup complete",
481
+ "note": "Full optimization requires running with optstrategy - see backtrader documentation",
482
+ "param_name": param_name,
483
+ "range": [start, end, step]
484
+ }
485
  except Exception as e:
486
+ return {"success": False, "error": str(e)}
487
 
488
+
489
+ # ============================================================================
490
+ # Information and Management Tools
491
+ # ============================================================================
492
+
493
+ @mcp.tool()
494
+ def list_cerebros() -> dict:
495
+ """
496
+ List all created Cerebro instances.
497
+
498
+ Returns:
499
+ - dict: List of cerebro IDs
500
+ """
501
+ if not BACKTRADER_AVAILABLE:
502
+ return {"success": False, "error": "backtrader not installed"}
503
+
504
  try:
505
+ cerebro_list = list(_cerebros.keys())
 
 
 
 
 
506
 
507
+ return {
508
+ "success": True,
509
+ "n_cerebros": len(cerebro_list),
510
+ "cerebro_ids": cerebro_list
511
+ }
512
+ except Exception as e:
513
+ return {"success": False, "error": str(e)}
514
+
515
+
516
+ @mcp.tool()
517
+ def delete_cerebro(cerebro_id: str) -> dict:
518
+ """
519
+ Delete a Cerebro instance from memory.
520
+
521
+ Parameters:
522
+ - cerebro_id: Cerebro instance ID
523
+
524
+ Returns:
525
+ - dict: Confirmation
526
+ """
527
+ if not BACKTRADER_AVAILABLE:
528
+ return {"success": False, "error": "backtrader not installed"}
529
+
530
+ if cerebro_id not in _cerebros:
531
+ return {"success": False, "error": f"Cerebro {cerebro_id} not found"}
532
+
533
+ try:
534
+ del _cerebros[cerebro_id]
535
+ return {"success": True, "message": f"Cerebro {cerebro_id} deleted"}
536
  except Exception as e:
537
+ return {"success": False, "error": str(e)}
538
+
539
+
540
+ @mcp.tool()
541
+ def get_backtrader_info() -> dict:
542
+ """
543
+ Get backtrader version and available features.
544
 
545
+ Returns:
546
+ - dict: Version and features
547
+ """
548
+ if not BACKTRADER_AVAILABLE:
549
+ return {
550
+ "success": False,
551
+ "available": False,
552
+ "message": "backtrader not installed. Install with: pip install backtrader"
553
+ }
554
+
555
  try:
556
+ return {
557
+ "success": True,
558
+ "available": True,
559
+ "version": bt.__version__,
560
+ "description": "Python backtesting library for trading strategies",
561
+ "features": [
562
+ "Strategy backtesting and optimization",
563
+ "Technical indicators (100+ built-in)",
564
+ "Multiple data feeds and timeframes",
565
+ "Portfolio analytics and risk metrics",
566
+ "Live trading integration",
567
+ "Custom indicators and strategies"
568
+ ],
569
+ "components": {
570
+ "Cerebro": "Main backtesting engine",
571
+ "Strategy": "Base class for trading strategies",
572
+ "Indicators": "Technical analysis indicators (SMA, RSI, MACD, etc.)",
573
+ "Analyzers": "Performance metrics (Sharpe, Returns, DrawDown)",
574
+ "Observers": "Visual tracking (Broker, Trades, BuySell)",
575
+ "Data Feeds": "CSV, Pandas, Live data sources"
576
+ },
577
+ "indicators": [
578
+ "SimpleMovingAverage (SMA)",
579
+ "ExponentialMovingAverage (EMA)",
580
+ "RelativeStrengthIndex (RSI)",
581
+ "MACD",
582
+ "BollingerBands",
583
+ "Stochastic",
584
+ "ATR (Average True Range)",
585
+ "And 100+ more..."
586
+ ],
587
+ "data_feeds": [
588
+ "CSV files",
589
+ "Pandas DataFrames",
590
+ "Live data (Interactive Brokers, OANDA, etc.)",
591
+ "Custom data sources"
592
+ ],
593
+ "website": "https://www.backtrader.com",
594
+ "documentation": "https://www.backtrader.com/docu/",
595
+ "github": "https://github.com/mementum/backtrader"
596
+ }
597
  except Exception as e:
598
+ return {"success": False, "error": str(e)}
599
 
600
 
601
+ def create_app() -> FastMCP:
602
+ """
603
+ Creates and returns the FastMCP instance for the backtrader service.
604
 
605
+ Returns:
606
+ - FastMCP: The FastMCP instance.
607
+ """
608
  return mcp
609
 
610
  if __name__ == "__main__":
611
+ mcp.run(transport="http", host="0.0.0.0", port=8000)