ghh1125 commited on
Commit
905fbb6
·
verified ·
1 Parent(s): be9d568

Upload 14 files

Browse files
Dockerfile CHANGED
@@ -1,18 +1,23 @@
1
- FROM python:3.10
2
 
3
- RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
- USER user
5
- ENV PATH="/home/user/.local/bin:$PATH"
6
 
7
  WORKDIR /app
8
 
9
- COPY --chown=user ./requirements.txt requirements.txt
10
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
 
 
11
 
12
- COPY --chown=user . /app
13
  ENV MCP_TRANSPORT=http
14
  ENV MCP_PORT=7860
15
 
16
  EXPOSE 7860
17
 
 
 
18
  CMD ["python", "backtrader/mcp_output/start_mcp.py"]
 
1
+ FROM python:3.11-slim
2
 
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1
 
5
 
6
  WORKDIR /app
7
 
8
+ RUN useradd -m -u 1000 appuser
9
+
10
+ COPY requirements.txt /app/requirements.txt
11
+ RUN pip install --no-cache-dir -r /app/requirements.txt
12
+
13
+ COPY backtrader /app/backtrader
14
+ COPY app.py /app/app.py
15
 
 
16
  ENV MCP_TRANSPORT=http
17
  ENV MCP_PORT=7860
18
 
19
  EXPOSE 7860
20
 
21
+ USER appuser
22
+
23
  CMD ["python", "backtrader/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,67 @@
1
  ---
2
- title: Backtrader
3
- emoji: 🦀
4
- colorFrom: green
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: backtrader MCP Service
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # backtrader MCP Service
12
+
13
+ This deployment package exposes selected local `backtrader` capabilities through MCP using FastMCP.
14
+
15
+ ## Available Tools
16
+
17
+ - `health_check`
18
+ - `list_modules`
19
+ - `list_symbols`
20
+ - `preview_csv_data`
21
+ - `create_cerebro_session`
22
+ - `run_sma_crossover_backtest`
23
+ - `list_builtin_components`
24
+ - `adapter_call_function`
25
+
26
+ Detailed tool documentation is available in `backtrader/mcp_output/README_MCP.md`.
27
+
28
+ ## Local stdio usage
29
+
30
+ 1. Install dependencies:
31
+ ```bash
32
+ pip install -r requirements.txt
33
+ ```
34
+ 2. Start stdio MCP service:
35
+ ```bash
36
+ python backtrader/mcp_output/start_mcp.py
37
+ ```
38
+
39
+ For direct local plugin entry:
40
+
41
+ ```bash
42
+ python backtrader/mcp_output/mcp_plugin/main.py
43
+ ```
44
+
45
+ ## HTTP usage (Docker / HF Spaces style)
46
+
47
+ Start with HTTP transport:
48
+
49
+ ```bash
50
+ MCP_TRANSPORT=http MCP_PORT=7860 python backtrader/mcp_output/start_mcp.py
51
+ ```
52
+
53
+ MCP endpoint:
54
+
55
+ - `http://localhost:7860/mcp`
56
+
57
+ ## Docker usage
58
+
59
+ ```bash
60
+ ./run_docker.sh
61
+ ```
62
+
63
+ or in PowerShell:
64
+
65
+ ```powershell
66
+ ./run_docker.ps1
67
+ ```
app.py CHANGED
@@ -1,45 +1,59 @@
1
- from fastapi import FastAPI
 
2
  import os
3
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- mcp_plugin_path = os.path.join(os.path.dirname(__file__), "backtrader", "mcp_output", "mcp_plugin")
6
- sys.path.insert(0, mcp_plugin_path)
 
 
7
 
8
- app = FastAPI(
9
- title="Backtrader MCP Service",
10
- description="Auto-generated MCP service for backtrader",
11
- version="1.0.0"
12
- )
13
 
14
  @app.get("/")
15
- def root():
16
  return {
17
- "service": "Backtrader MCP Service",
18
- "version": "1.0.0",
19
- "status": "running",
20
- "transport": os.environ.get("MCP_TRANSPORT", "http")
21
  }
22
 
 
23
  @app.get("/health")
24
- def health_check():
25
- return {"status": "healthy", "service": "backtrader MCP"}
 
26
 
27
  @app.get("/tools")
28
- def list_tools():
29
- try:
30
- from mcp_service import create_app
31
- mcp_app = create_app()
32
- tools = []
33
- for tool_name, tool_func in mcp_app.tools.items():
34
- tools.append({
35
- "name": tool_name,
36
- "description": tool_func.__doc__ or "No description available"
37
- })
38
- return {"tools": tools}
39
- except Exception as e:
40
- return {"error": f"Failed to load tools: {str(e)}"}
41
 
42
  if __name__ == "__main__":
43
  import uvicorn
44
- port = int(os.environ.get("PORT", 7860))
 
45
  uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ from __future__ import annotations
2
+
3
  import os
4
  import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from fastapi import FastAPI
9
+
10
+ BASE_DIR = Path(__file__).resolve().parent
11
+ PLUGIN_DIR = BASE_DIR / "backtrader" / "mcp_output" / "mcp_plugin"
12
+
13
+ if str(PLUGIN_DIR) not in sys.path:
14
+ sys.path.insert(0, str(PLUGIN_DIR))
15
+
16
+ app = FastAPI(title="backtrader-mcp-info", version="1.0.0")
17
+
18
 
19
+ def _extract_tool_info(tool_obj: Any) -> dict[str, str]:
20
+ name = getattr(tool_obj, "name", None) or getattr(tool_obj, "__name__", "unknown")
21
+ description = getattr(tool_obj, "description", None) or getattr(tool_obj, "__doc__", "") or ""
22
+ return {"name": str(name), "description": str(description).strip()}
23
 
 
 
 
 
 
24
 
25
  @app.get("/")
26
+ def root() -> dict[str, Any]:
27
  return {
28
+ "service": "backtrader MCP deployment",
29
+ "mcp_transport": os.getenv("MCP_TRANSPORT", "stdio"),
30
+ "mcp_port": int(os.getenv("MCP_PORT", "8000")),
31
+ "note": "This FastAPI app is supplementary and does not run the MCP server.",
32
  }
33
 
34
+
35
  @app.get("/health")
36
+ def health() -> dict[str, str]:
37
+ return {"status": "healthy"}
38
+
39
 
40
  @app.get("/tools")
41
+ def tools() -> dict[str, Any]:
42
+ from mcp_service import create_app
43
+
44
+ mcp = create_app()
45
+ raw_tools = getattr(mcp, "tools", [])
46
+
47
+ if isinstance(raw_tools, dict):
48
+ items = [_extract_tool_info(value) for value in raw_tools.values()]
49
+ else:
50
+ items = [_extract_tool_info(item) for item in raw_tools]
51
+
52
+ return {"count": len(items), "tools": items}
53
+
54
 
55
  if __name__ == "__main__":
56
  import uvicorn
57
+
58
+ port = int(os.getenv("PORT", "7860"))
59
  uvicorn.run(app, host="0.0.0.0", port=port)
backtrader/mcp_output/README_MCP.md CHANGED
@@ -1,147 +1,112 @@
1
- # Backtrader MCP (Model Context Protocol) Service README
2
-
3
- ## 1) Project Introduction
4
-
5
- This MCP (Model Context Protocol) service exposes the core capabilities of the `backtrader` engine for strategy research and backtesting through callable service endpoints.
6
-
7
- Main functions:
8
- - Run backtests with `Cerebro`
9
- - Load market data (CSV, Pandas, Yahoo/Quandl-style feeds, and others)
10
- - Execute strategies, brokers, sizers, analyzers, and observers
11
- - Return structured performance outputs (returns, drawdown, Sharpe, trade stats, etc.)
12
- - Support optional plotting and integration-oriented workflows
13
-
14
- Repository analyzed: https://github.com/mementum/backtrader
15
-
16
- ---
17
-
18
- ## 2) Installation Method
19
-
20
- ### Requirements
21
- - Python runtime
22
- - Required: `matplotlib`
23
- - Common optional deps: `pandas`, `numpy`, `python-dateutil`, `pytz`
24
- - Integration-specific optional deps:
25
- - IB stack (`ibpy`/`ib_insync` ecosystem) for IB-related store/broker/data
26
- - Oanda client libs for Oanda store/broker/data
27
- - `TA-Lib` for `backtrader.talib`
28
- - `pyfolio` for PyFolio analyzer workflows
29
-
30
- ### Install
31
- - Install backtrader:
32
- pip install backtrader
33
-
34
- - Minimal plotting dependency:
35
- pip install matplotlib
36
-
37
- - Recommended data stack:
38
- pip install pandas numpy python-dateutil pytz
39
-
40
- ---
41
-
42
- ## 3) Quick Start
43
-
44
- ### Minimal service workflow
45
- 1. Create a backtest run request
46
- 2. Provide data source (e.g., CSV or Pandas)
47
- 3. Select strategy and parameters
48
- 4. Attach analyzers (Sharpe, DrawDown, Returns, TradeAnalyzer, etc.)
49
- 5. Execute and fetch normalized results
50
-
51
- ### Typical Python usage pattern behind the service
52
- import backtrader as bt
53
-
54
- class SmaCross(bt.Strategy):
55
- params = dict(fast=10, slow=30)
56
- def __init__(self):
57
- sma1 = bt.ind.SMA(period=self.p.fast)
58
- sma2 = bt.ind.SMA(period=self.p.slow)
59
- self.crossover = bt.ind.CrossOver(sma1, sma2)
60
- def next(self):
61
- if not self.position and self.crossover > 0:
62
- self.buy()
63
- elif self.position and self.crossover < 0:
64
- self.close()
65
-
66
- cerebro = bt.Cerebro()
67
- cerebro.addstrategy(SmaCross)
68
- data = bt.feeds.GenericCSVData(dataname="datas/2006-day-001.txt")
69
- cerebro.adddata(data)
70
- cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe")
71
- cerebro.run()
72
-
73
- In the MCP (Model Context Protocol) service, this is wrapped into endpoint calls (see below).
74
-
75
- ---
76
-
77
- ## 4) Available Tools and Endpoints List
78
-
79
- Recommended service endpoints (developer-oriented mapping to backtrader modules):
80
-
81
- - `health`
82
- - Basic readiness/liveness check.
83
-
84
- - `list_capabilities`
85
- - Returns supported feeds, analyzers, observers, sizers, and integration availability.
86
-
87
- - `run_backtest`
88
- - Core endpoint. Runs a backtest with:
89
- - strategy class/name + params
90
- - data config (feed type, path, timeframe/compression)
91
- - broker/cash/commission/slippage
92
- - analyzers/observers/sizers
93
- - Returns metrics, trades, and optional artifacts metadata.
94
-
95
- - `optimize_strategy`
96
- - Runs parameter sweeps/optimization over strategy params.
97
- - Returns ranked result set and top configurations.
98
-
99
- - `load_data_preview`
100
- - Validates data config and previews parsed bars/date range before execution.
101
-
102
- - `list_analyzers`
103
- - Lists available analyzers (e.g., `SharpeRatio`, `DrawDown`, `Returns`, `TradeAnalyzer`, `TimeReturn`, `SQN`, `VWR`).
104
-
105
- - `list_feeds`
106
- - Lists supported feed adapters (CSV generic/backtrader CSV, Pandas, Yahoo/Quandl-style, IB/Oanda/VC when enabled).
107
-
108
- - `run_btrun_cli` (optional bridge)
109
- - Wraps `backtrader.btrun.btrun` style execution for CLI-compatible scenarios.
110
-
111
- ---
112
-
113
- ## 5) Common Issues and Notes
114
-
115
- - Dependency gaps:
116
- - Core backtesting works with minimal deps, but many feeds/integrations need extra packages.
117
- - TA-Lib and PyFolio:
118
- - Optional; endpoint should gracefully report “not installed” capability.
119
- - Broker/store integrations:
120
- - IB/Oanda/VC modules require external vendor/client ecosystems and credentials.
121
- - Data alignment/timeframes:
122
- - Multi-timeframe and replay/resample logic can be sensitive to feed configuration.
123
- - Plotting in server environments:
124
- - Prefer non-interactive backends or disable plotting in headless deployments.
125
- - Performance:
126
- - Strategy optimization can be CPU-heavy; use bounded parameter grids and job limits.
127
- - Stability:
128
- - Import feasibility is high and intrusiveness risk is low per analysis, but integration modules vary by environment.
129
-
130
- ---
131
-
132
- ## 6) Reference Links / Documentation
133
-
134
- - Upstream repository: https://github.com/mementum/backtrader
135
- - Package entry points of interest:
136
- - `backtrader.cerebro.Cerebro`
137
- - `backtrader.strategy.Strategy`
138
- - `backtrader.feed` / `backtrader.feeds.*`
139
- - `backtrader.analyzers.*`
140
- - `backtrader.observers.*`
141
- - `backtrader.btrun.btrun`
142
- - Samples directory (practical patterns):
143
- - `samples/` in repository root
144
- - Tests directory (behavior references):
145
- - `tests/` in repository root
146
-
147
- If you want, I can also generate a concrete endpoint I/O schema (JSON request/response shapes) for each MCP (Model Context Protocol) service endpoint.
 
1
+ # Backtrader MCP Plugin
2
+
3
+ This MCP plugin exposes core local backtesting capabilities from the `backtrader` library.
4
+
5
+ ## Exposed Tools
6
+
7
+ ### 1) `health_check`
8
+ - **Description**: Check service and dependency availability.
9
+ - **Parameters**: none
10
+ - **Example**:
11
+ ```json
12
+ {"tool":"health_check","arguments":{}}
13
+ ```
14
+
15
+ ### 2) `list_modules`
16
+ - **Description**: List loaded and failed `backtrader` modules tracked by the adapter.
17
+ - **Parameters**: none
18
+ - **Example**:
19
+ ```json
20
+ {"tool":"list_modules","arguments":{}}
21
+ ```
22
+
23
+ ### 3) `list_symbols`
24
+ - **Description**: Inspect symbols exported by a loaded module.
25
+ - **Parameters**:
26
+ - `module_name` (string): module name, e.g. `backtrader.indicators`
27
+ - `include_private` (boolean, optional)
28
+ - `limit` (integer, optional)
29
+ - **Example**:
30
+ ```json
31
+ {"tool":"list_symbols","arguments":{"module_name":"backtrader.indicators","limit":30}}
32
+ ```
33
+
34
+ ### 4) `preview_csv_data`
35
+ - **Description**: Read a quick sample of local CSV rows.
36
+ - **Parameters**:
37
+ - `data_path` (string): absolute or relative CSV path
38
+ - `delimiter` (string, optional)
39
+ - `max_rows` (integer, optional)
40
+ - **Example**:
41
+ ```json
42
+ {"tool":"preview_csv_data","arguments":{"data_path":"2006-day-001.txt","delimiter":",","max_rows":5}}
43
+ ```
44
+
45
+ ### 5) `create_cerebro_session`
46
+ - **Description**: Create and summarize a baseline `Cerebro` session.
47
+ - **Parameters**:
48
+ - `initial_cash` (number, optional)
49
+ - `commission` (number, optional)
50
+ - **Example**:
51
+ ```json
52
+ {"tool":"create_cerebro_session","arguments":{"initial_cash":10000,"commission":0.001}}
53
+ ```
54
+
55
+ ### 6) `run_sma_crossover_backtest`
56
+ - **Description**: Run a simple SMA crossover strategy on local CSV data.
57
+ - **Parameters**:
58
+ - `data_path` (string)
59
+ - `fast_period` (integer, optional)
60
+ - `slow_period` (integer, optional)
61
+ - `initial_cash` (number, optional)
62
+ - `commission` (number, optional)
63
+ - `stake` (integer, optional)
64
+ - **Example**:
65
+ ```json
66
+ {"tool":"run_sma_crossover_backtest","arguments":{"data_path":"2006-day-001.txt","fast_period":10,"slow_period":30}}
67
+ ```
68
+
69
+ ### 7) `list_builtin_components`
70
+ - **Description**: List built-in indicators, analyzers, or sizers.
71
+ - **Parameters**:
72
+ - `component_type` (string): `indicators` | `analyzers` | `sizers`
73
+ - `limit` (integer, optional)
74
+ - **Example**:
75
+ ```json
76
+ {"tool":"list_builtin_components","arguments":{"component_type":"indicators","limit":25}}
77
+ ```
78
+
79
+ ### 8) `adapter_call_function`
80
+ - **Description**: Generic adapter function call for advanced introspection.
81
+ - **Parameters**:
82
+ - `module_name` (string)
83
+ - `function_name` (string)
84
+ - `positional_args_json` (string, optional JSON array)
85
+ - `keyword_args_json` (string, optional JSON object)
86
+ - **Example**:
87
+ ```json
88
+ {"tool":"adapter_call_function","arguments":{"module_name":"backtrader.mathsupport","function_name":"average","positional_args_json":"[[1,2,3]]"}}
89
+ ```
90
+
91
+ ## Running Locally (stdio)
92
+
93
+ ```bash
94
+ cd backtrader/mcp_output
95
+ python start_mcp.py
96
+ ```
97
+
98
+ Or run the explicit stdio local entry point:
99
+
100
+ ```bash
101
+ cd backtrader/mcp_output/mcp_plugin
102
+ python main.py
103
+ ```
104
+
105
+ ## Running via HTTP
106
+
107
+ ```bash
108
+ cd backtrader/mcp_output
109
+ MCP_TRANSPORT=http MCP_PORT=7860 python start_mcp.py
110
+ ```
111
+
112
+ The MCP HTTP endpoint is served at `/mcp`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backtrader/mcp_output/mcp_plugin/adapter.py CHANGED
@@ -1,472 +1,193 @@
1
- import os
2
- import sys
3
- import importlib
4
- import importlib.util
5
- import importlib.machinery
6
- from typing import Any, Dict, Optional, Tuple
7
-
8
- source_path = os.path.join(
9
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
10
- "source",
11
- )
12
- sys.path.insert(0, source_path)
13
 
 
 
 
 
 
 
 
14
 
15
- class Adapter:
16
- """
17
- MCP Import Mode Adapter for backtrader repository integration.
18
-
19
- This adapter attempts direct module import first ("import" mode) and falls back
20
- to a file-based dynamic loader ("fallback_cli") when needed.
21
 
22
- Unified return format for all public methods:
23
- {
24
- "status": "success" | "error",
25
- "mode": "<current mode>",
26
- "message": "<human-readable summary>",
27
- "data": <optional payload>,
28
- "error": "<optional error details>"
29
- }
30
- """
31
 
32
- def __init__(self) -> None:
33
- self.mode = "import"
34
- self._loaded_modules: Dict[str, Any] = {}
35
- self._import_errors: Dict[str, str] = {}
36
- self._initialize_imports()
37
 
38
- # -------------------------------------------------------------------------
39
- # Core helpers
40
- # -------------------------------------------------------------------------
41
- def _ok(self, message: str, data: Optional[Any] = None) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  return {
43
- "status": "success",
44
- "mode": self.mode,
45
- "message": message,
46
- "data": data,
 
 
 
 
47
  }
48
 
49
- def _err(self, message: str, error: Optional[Exception] = None) -> Dict[str, Any]:
 
50
  return {
51
- "status": "error",
52
- "mode": self.mode,
53
- "message": message,
54
- "error": str(error) if error else None,
55
  }
56
 
57
- def _safe_import(self, module_path: str) -> Tuple[Optional[Any], Optional[str]]:
58
- try:
59
- module = importlib.import_module(module_path)
60
- self._loaded_modules[module_path] = module
61
- return module, None
62
- except Exception as exc:
63
- self._import_errors[module_path] = str(exc)
64
- return None, str(exc)
65
-
66
- def _load_module_from_file(self, module_name: str, file_path: str) -> Tuple[Optional[Any], Optional[str]]:
67
- try:
68
- if not os.path.exists(file_path):
69
- return None, f"File not found: {file_path}. Verify repository extraction path and source directory."
70
- loader = importlib.machinery.SourceFileLoader(module_name, file_path)
71
- spec = importlib.util.spec_from_loader(module_name, loader)
72
- if spec is None:
73
- return None, f"Unable to create module spec for {module_name}."
74
- mod = importlib.util.module_from_spec(spec)
75
- loader.exec_module(mod)
76
- self._loaded_modules[module_name] = mod
77
- return mod, None
78
- except Exception as exc:
79
- return None, str(exc)
80
-
81
- def _initialize_imports(self) -> None:
82
- targets = [
83
- "backtrader.btrun.btrun",
84
- "tools.rewrite-data",
85
- "tools.yahoodownload",
86
- "contrib.utils.iqfeed-to-influxdb",
87
- "contrib.utils.influxdb-import",
88
- "contrib.samples.pair-trading.pair-trading",
89
- "samples.weekdays-filler.weekdaysfiller",
90
- "samples.weekdays-filler.weekdaysaligner",
91
- ]
92
-
93
- for mod in targets:
94
- self._safe_import(mod)
95
-
96
- if any(m not in self._loaded_modules for m in targets):
97
- self.mode = "fallback_cli"
98
-
99
- def get_health(self) -> Dict[str, Any]:
100
- """
101
- Return adapter health and module import diagnostics.
102
-
103
- Returns:
104
- dict: Unified status payload with import success/failure overview.
105
- """
106
- return self._ok(
107
- "Adapter health check completed.",
108
- data={
109
- "loaded_modules": list(self._loaded_modules.keys()),
110
- "import_errors": self._import_errors,
111
- "source_path": source_path,
112
- },
113
- )
114
-
115
- # -------------------------------------------------------------------------
116
- # backtrader.btrun.btrun
117
- # -------------------------------------------------------------------------
118
- def call_btrun(self, argv: Optional[list] = None) -> Dict[str, Any]:
119
- """
120
- Execute the built-in btrun command module.
121
-
122
- Args:
123
- argv (list, optional): Command-line style arguments. If omitted, module defaults are used.
124
-
125
- Returns:
126
- dict: Unified status with execution result or actionable error guidance.
127
- """
128
- try:
129
- module = self._loaded_modules.get("backtrader.btrun.btrun")
130
- if module is None:
131
- module, err = self._safe_import("backtrader.btrun.btrun")
132
- if module is None:
133
- return self._err(
134
- "Failed to import backtrader btrun module. Confirm source/backtrader is present and importable.",
135
- Exception(err),
136
- )
137
-
138
- if hasattr(module, "main"):
139
- result = module.main(argv) if argv is not None else module.main()
140
- return self._ok("btrun executed via main().", data={"result": result})
141
-
142
- return self._err(
143
- "btrun module does not expose main(). Use backtrader.btrun.btrun manually with repository-compatible arguments."
144
- )
145
- except Exception as exc:
146
- return self._err("btrun execution failed. Validate arguments and data file paths.", exc)
147
-
148
- # -------------------------------------------------------------------------
149
- # tools/rewrite-data.py
150
- # -------------------------------------------------------------------------
151
- def call_rewrite_data_parse_args(self, args: Optional[list] = None) -> Dict[str, Any]:
152
- """
153
- Call parse_args from tools/rewrite-data.py.
154
-
155
- Args:
156
- args (list, optional): Argument vector for parser input.
157
-
158
- Returns:
159
- dict: Unified status with parser namespace/details.
160
- """
161
- try:
162
- mod = self._loaded_modules.get("tools.rewrite-data")
163
- if mod is None:
164
- path = os.path.join(source_path, "tools", "rewrite-data.py")
165
- mod, err = self._load_module_from_file("tools.rewrite_data_dynamic", path)
166
- if mod is None:
167
- return self._err("Unable to load tools/rewrite-data.py for parse_args.", Exception(err))
168
-
169
- fn = getattr(mod, "parse_args", None)
170
- if fn is None:
171
- return self._err("parse_args not found in tools/rewrite-data.py. Confirm repository version compatibility.")
172
- res = fn(args) if args is not None else fn()
173
- return self._ok("rewrite-data parse_args executed.", data={"result": res})
174
- except Exception as exc:
175
- return self._err("rewrite-data parse_args failed. Check argument format.", exc)
176
-
177
- def call_rewrite_data_runstrat(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
178
- """
179
- Call runstrat from tools/rewrite-data.py.
180
-
181
- Args:
182
- *args: Positional arguments forwarded to runstrat.
183
- **kwargs: Keyword arguments forwarded to runstrat.
184
-
185
- Returns:
186
- dict: Unified status with execution output.
187
- """
188
- try:
189
- mod = self._loaded_modules.get("tools.rewrite-data")
190
- if mod is None:
191
- path = os.path.join(source_path, "tools", "rewrite-data.py")
192
- mod, err = self._load_module_from_file("tools.rewrite_data_dynamic", path)
193
- if mod is None:
194
- return self._err("Unable to load tools/rewrite-data.py for runstrat.", Exception(err))
195
-
196
- fn = getattr(mod, "runstrat", None)
197
- if fn is None:
198
- return self._err("runstrat not found in tools/rewrite-data.py. Confirm repository version compatibility.")
199
- res = fn(*args, **kwargs)
200
- return self._ok("rewrite-data runstrat executed.", data={"result": res})
201
- except Exception as exc:
202
- return self._err("rewrite-data runstrat failed. Validate strategy/data parameters.", exc)
203
-
204
- # -------------------------------------------------------------------------
205
- # tools/yahoodownload.py
206
- # -------------------------------------------------------------------------
207
- def create_yahoo_download_instance(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
208
- """
209
- Instantiate YahooDownload from tools/yahoodownload.py.
210
-
211
- Args:
212
- *args: Constructor positional arguments.
213
- **kwargs: Constructor keyword arguments.
214
-
215
- Returns:
216
- dict: Unified status with instantiated object.
217
- """
218
- try:
219
- mod = self._loaded_modules.get("tools.yahoodownload")
220
- if mod is None:
221
- mod, err = self._safe_import("tools.yahoodownload")
222
- if mod is None:
223
- return self._err("Unable to import tools.yahoodownload.", Exception(err))
224
-
225
- cls = getattr(mod, "YahooDownload", None)
226
- if cls is None:
227
- return self._err("YahooDownload class not found in tools.yahoodownload.")
228
- inst = cls(*args, **kwargs)
229
- return self._ok("YahooDownload instance created.", data={"instance": inst})
230
- except Exception as exc:
231
- return self._err("Failed to create YahooDownload instance. Check constructor parameters.", exc)
232
-
233
- def call_yahoodownload_parse_args(self, args: Optional[list] = None) -> Dict[str, Any]:
234
- """
235
- Call parse_args from tools/yahoodownload.py.
236
-
237
- Args:
238
- args (list, optional): Argument vector for parser input.
239
-
240
- Returns:
241
- dict: Unified status with parser output.
242
- """
243
- try:
244
- mod = self._loaded_modules.get("tools.yahoodownload")
245
- if mod is None:
246
- mod, err = self._safe_import("tools.yahoodownload")
247
- if mod is None:
248
- return self._err("Unable to import tools.yahoodownload.", Exception(err))
249
-
250
- fn = getattr(mod, "parse_args", None)
251
- if fn is None:
252
- return self._err("parse_args not found in tools.yahoodownload.")
253
- res = fn(args) if args is not None else fn()
254
- return self._ok("yahoodownload parse_args executed.", data={"result": res})
255
- except Exception as exc:
256
- return self._err("yahoodownload parse_args failed. Verify CLI-style arguments.", exc)
257
-
258
- # -------------------------------------------------------------------------
259
- # contrib/utils/iqfeed-to-influxdb.py
260
- # -------------------------------------------------------------------------
261
- def create_iqfeed_tool_instance(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
262
- """
263
- Instantiate IQFeedTool from contrib/utils/iqfeed-to-influxdb.py.
264
-
265
- Args:
266
- *args: Constructor positional arguments.
267
- **kwargs: Constructor keyword arguments.
268
-
269
- Returns:
270
- dict: Unified status with created instance.
271
- """
272
- try:
273
- mod = self._loaded_modules.get("contrib.utils.iqfeed-to-influxdb")
274
- if mod is None:
275
- path = os.path.join(source_path, "contrib", "utils", "iqfeed-to-influxdb.py")
276
- mod, err = self._load_module_from_file("contrib.utils.iqfeed_to_influxdb_dynamic", path)
277
- if mod is None:
278
- return self._err("Unable to load contrib/utils/iqfeed-to-influxdb.py.", Exception(err))
279
-
280
- cls = getattr(mod, "IQFeedTool", None)
281
- if cls is None:
282
- return self._err("IQFeedTool class not found in contrib/utils/iqfeed-to-influxdb.py.")
283
- inst = cls(*args, **kwargs)
284
- return self._ok("IQFeedTool instance created.", data={"instance": inst})
285
- except Exception as exc:
286
- return self._err("Failed to create IQFeedTool instance. Validate required external service settings.", exc)
287
 
288
- # -------------------------------------------------------------------------
289
- # contrib/utils/influxdb-import.py
290
- # -------------------------------------------------------------------------
291
- def create_influxdb_tool_instance(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
292
- """
293
- Instantiate InfluxDBTool from contrib/utils/influxdb-import.py.
294
-
295
- Args:
296
- *args: Constructor positional arguments.
297
- **kwargs: Constructor keyword arguments.
298
-
299
- Returns:
300
- dict: Unified status with created instance.
301
- """
302
- try:
303
- mod = self._loaded_modules.get("contrib.utils.influxdb-import")
304
- if mod is None:
305
- path = os.path.join(source_path, "contrib", "utils", "influxdb-import.py")
306
- mod, err = self._load_module_from_file("contrib.utils.influxdb_import_dynamic", path)
307
- if mod is None:
308
- return self._err("Unable to load contrib/utils/influxdb-import.py.", Exception(err))
309
-
310
- cls = getattr(mod, "InfluxDBTool", None)
311
- if cls is None:
312
- return self._err("InfluxDBTool class not found in contrib/utils/influxdb-import.py.")
313
- inst = cls(*args, **kwargs)
314
- return self._ok("InfluxDBTool instance created.", data={"instance": inst})
315
- except Exception as exc:
316
- return self._err("Failed to create InfluxDBTool instance. Verify InfluxDB connection and credentials.", exc)
317
-
318
- # -------------------------------------------------------------------------
319
- # contrib/samples/pair-trading/pair-trading.py
320
- # -------------------------------------------------------------------------
321
- def create_pair_trading_strategy_instance(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
322
- """
323
- Instantiate PairTradingStrategy from contrib/samples/pair-trading/pair-trading.py.
324
-
325
- Args:
326
- *args: Constructor positional arguments.
327
- **kwargs: Constructor keyword arguments.
328
-
329
- Returns:
330
- dict: Unified status with created strategy instance.
331
- """
332
- try:
333
- mod = self._loaded_modules.get("contrib.samples.pair-trading.pair-trading")
334
- if mod is None:
335
- path = os.path.join(source_path, "contrib", "samples", "pair-trading", "pair-trading.py")
336
- mod, err = self._load_module_from_file("contrib.samples.pair_trading_dynamic", path)
337
- if mod is None:
338
- return self._err("Unable to load pair-trading sample module.", Exception(err))
339
-
340
- cls = getattr(mod, "PairTradingStrategy", None)
341
- if cls is None:
342
- return self._err("PairTradingStrategy class not found in pair-trading sample module.")
343
- inst = cls(*args, **kwargs)
344
- return self._ok("PairTradingStrategy instance created.", data={"instance": inst})
345
- except Exception as exc:
346
- return self._err("Failed to create PairTradingStrategy instance. Use Cerebro-managed instantiation for runtime use.", exc)
347
-
348
- def call_pair_trading_parse_args(self, args: Optional[list] = None) -> Dict[str, Any]:
349
- """
350
- Call parse_args from pair-trading sample module.
351
-
352
- Args:
353
- args (list, optional): Parser argument list.
354
-
355
- Returns:
356
- dict: Unified status with parser result.
357
- """
358
- try:
359
- path = os.path.join(source_path, "contrib", "samples", "pair-trading", "pair-trading.py")
360
- mod, err = self._load_module_from_file("contrib.samples.pair_trading_dynamic", path)
361
- if mod is None:
362
- return self._err("Unable to load pair-trading sample for parse_args.", Exception(err))
363
-
364
- fn = getattr(mod, "parse_args", None)
365
- if fn is None:
366
- return self._err("parse_args not found in pair-trading sample module.")
367
- res = fn(args) if args is not None else fn()
368
- return self._ok("pair-trading parse_args executed.", data={"result": res})
369
- except Exception as exc:
370
- return self._err("pair-trading parse_args failed. Verify provided options.", exc)
371
-
372
- def call_pair_trading_runstrategy(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
373
- """
374
- Call runstrategy from pair-trading sample module.
375
-
376
- Args:
377
- *args: Positional arguments for runstrategy.
378
- **kwargs: Keyword arguments for runstrategy.
379
-
380
- Returns:
381
- dict: Unified status with run output.
382
- """
383
- try:
384
- path = os.path.join(source_path, "contrib", "samples", "pair-trading", "pair-trading.py")
385
- mod, err = self._load_module_from_file("contrib.samples.pair_trading_dynamic", path)
386
- if mod is None:
387
- return self._err("Unable to load pair-trading sample for runstrategy.", Exception(err))
388
-
389
- fn = getattr(mod, "runstrategy", None)
390
- if fn is None:
391
- return self._err("runstrategy not found in pair-trading sample module.")
392
- res = fn(*args, **kwargs)
393
- return self._ok("pair-trading runstrategy executed.", data={"result": res})
394
- except Exception as exc:
395
- return self._err("pair-trading runstrategy failed. Check data feeds and broker settings.", exc)
396
-
397
- # -------------------------------------------------------------------------
398
- # samples/weekdays-filler
399
- # -------------------------------------------------------------------------
400
- def create_weekdays_filler_instance(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
401
- """
402
- Instantiate WeekDaysFiller from samples/weekdays-filler/weekdaysfiller.py.
403
 
404
- Args:
405
- *args: Constructor positional arguments.
406
- **kwargs: Constructor keyword arguments.
 
 
 
 
 
 
 
407
 
408
- Returns:
409
- dict: Unified status with created instance.
410
- """
411
- try:
412
- path = os.path.join(source_path, "samples", "weekdays-filler", "weekdaysfiller.py")
413
- mod, err = self._load_module_from_file("samples.weekdays_filler_dynamic", path)
414
- if mod is None:
415
- return self._err("Unable to load weekdaysfiller sample module.", Exception(err))
416
 
417
- cls = getattr(mod, "WeekDaysFiller", None)
418
- if cls is None:
419
- return self._err("WeekDaysFiller class not found in weekdaysfiller sample module.")
420
- inst = cls(*args, **kwargs)
421
- return self._ok("WeekDaysFiller instance created.", data={"instance": inst})
422
- except Exception as exc:
423
- return self._err("Failed to create WeekDaysFiller instance. Validate constructor inputs.", exc)
424
 
425
- def call_weekdaysaligner_parse_args(self, args: Optional[list] = None) -> Dict[str, Any]:
426
- """
427
- Call parse_args from samples/weekdays-filler/weekdaysaligner.py.
428
 
429
- Args:
430
- args (list, optional): Parser argument list.
431
-
432
- Returns:
433
- dict: Unified status with parser namespace/output.
434
- """
435
  try:
436
- path = os.path.join(source_path, "samples", "weekdays-filler", "weekdaysaligner.py")
437
- mod, err = self._load_module_from_file("samples.weekdays_aligner_dynamic", path)
438
- if mod is None:
439
- return self._err("Unable to load weekdaysaligner sample module.", Exception(err))
440
-
441
- fn = getattr(mod, "parse_args", None)
442
- if fn is None:
443
- return self._err("parse_args not found in weekdaysaligner sample module.")
444
- res = fn(args) if args is not None else fn()
445
- return self._ok("weekdaysaligner parse_args executed.", data={"result": res})
446
  except Exception as exc:
447
- return self._err("weekdaysaligner parse_args failed. Review argument values.", exc)
448
-
449
- def call_weekdaysaligner_runstrat(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
450
- """
451
- Call runstrat from samples/weekdays-filler/weekdaysaligner.py.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
- Args:
454
- *args: Positional args forwarded to runstrat.
455
- **kwargs: Keyword args forwarded to runstrat.
456
-
457
- Returns:
458
- dict: Unified status with strategy run result.
459
- """
460
  try:
461
- path = os.path.join(source_path, "samples", "weekdays-filler", "weekdaysaligner.py")
462
- mod, err = self._load_module_from_file("samples.weekdays_aligner_dynamic", path)
463
- if mod is None:
464
- return self._err("Unable to load weekdaysaligner sample module.", Exception(err))
465
-
466
- fn = getattr(mod, "runstrat", None)
467
- if fn is None:
468
- return self._err("runstrat not found in weekdaysaligner sample module.")
469
- res = fn(*args, **kwargs)
470
- return self._ok("weekdaysaligner runstrat executed.", data={"result": res})
471
  except Exception as exc:
472
- return self._err("weekdaysaligner runstrat failed. Verify feed and calendar options.", exc)
 
 
 
 
 
 
1
+ from __future__ import annotations
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ import importlib
4
+ import inspect
5
+ import pkgutil
6
+ import sys
7
+ from pathlib import Path
8
+ from types import ModuleType
9
+ from typing import Any
10
 
11
+ _PLUGIN_DIR = Path(__file__).resolve().parent
12
+ _REPO_DIR = _PLUGIN_DIR.parent.parent
13
+ _SOURCE_DIR = _REPO_DIR / "source"
 
 
 
14
 
15
+ if str(_SOURCE_DIR) not in sys.path:
16
+ sys.path.insert(0, str(_SOURCE_DIR))
 
 
 
 
 
 
 
17
 
 
 
 
 
 
18
 
19
+ class Adapter:
20
+ def __init__(self, package_name: str = "backtrader") -> None:
21
+ self.package_name = package_name
22
+ self._root_module: ModuleType | None = None
23
+ self._loaded_modules: dict[str, ModuleType] = {}
24
+ self._failed_modules: dict[str, str] = {}
25
+ self._mode = "normal"
26
+ self._bootstrap()
27
+
28
+ def _bootstrap(self) -> None:
29
+ try:
30
+ self._root_module = importlib.import_module(self.package_name)
31
+ self._loaded_modules[self.package_name] = self._root_module
32
+ except Exception as exc:
33
+ self._mode = "blackbox"
34
+ self._failed_modules[self.package_name] = str(exc)
35
+ return
36
+
37
+ self._import_submodules()
38
+ if len(self._loaded_modules) <= 0:
39
+ self._mode = "blackbox"
40
+
41
+ def _import_submodules(self) -> None:
42
+ if self._root_module is None:
43
+ return
44
+
45
+ module_paths = getattr(self._root_module, "__path__", None)
46
+ if not module_paths:
47
+ return
48
+
49
+ for module_info in pkgutil.walk_packages(module_paths, prefix=f"{self.package_name}."):
50
+ module_name = module_info.name
51
+ if module_name in self._loaded_modules:
52
+ continue
53
+
54
+ try:
55
+ module = importlib.import_module(module_name)
56
+ self._loaded_modules[module_name] = module
57
+ except Exception as exc:
58
+ self._failed_modules[module_name] = str(exc)
59
+
60
+ if not self._loaded_modules:
61
+ self._mode = "blackbox"
62
+
63
+ def health(self) -> dict[str, Any]:
64
+ status = "ok" if self._loaded_modules else "fallback"
65
  return {
66
+ "status": status,
67
+ "mode": self._mode,
68
+ "package": self.package_name,
69
+ "source_dir": str(_SOURCE_DIR),
70
+ "loaded_count": len(self._loaded_modules),
71
+ "failed_count": len(self._failed_modules),
72
+ "loaded_modules": sorted(self._loaded_modules.keys()),
73
+ "failed_modules": self._failed_modules,
74
  }
75
 
76
+ def list_modules(self) -> dict[str, Any]:
77
+ status = "ok" if self._loaded_modules else "fallback"
78
  return {
79
+ "status": status,
80
+ "mode": self._mode,
81
+ "loaded": sorted(self._loaded_modules.keys()),
82
+ "failed": self._failed_modules,
83
  }
84
 
85
+ def list_symbols(self, module_name: str, include_private: bool = False, limit: int = 200) -> dict[str, Any]:
86
+ module = self._loaded_modules.get(module_name)
87
+ if module is None:
88
+ error_message = f"Module not loaded: {module_name}"
89
+ if module_name in self._failed_modules:
90
+ error_message = f"Module failed to import: {module_name} ({self._failed_modules[module_name]})"
91
+ return {"status": "error", "error": error_message}
92
+
93
+ names = dir(module)
94
+ if not include_private:
95
+ names = [name for name in names if not name.startswith("_")]
96
+
97
+ symbols: list[dict[str, str]] = []
98
+ for name in names[: max(limit, 0)]:
99
+ try:
100
+ value = getattr(module, name)
101
+ if inspect.isclass(value):
102
+ kind = "class"
103
+ elif inspect.isfunction(value) or inspect.ismethod(value):
104
+ kind = "function"
105
+ elif inspect.ismodule(value):
106
+ kind = "module"
107
+ else:
108
+ kind = "value"
109
+ symbols.append({"name": name, "type": kind})
110
+ except Exception:
111
+ symbols.append({"name": name, "type": "unknown"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ return {
114
+ "status": "ok",
115
+ "module": module_name,
116
+ "count": len(symbols),
117
+ "symbols": symbols,
118
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ def call_function(
121
+ self,
122
+ module_name: str,
123
+ function_name: str,
124
+ positional_args: list[Any] | None = None,
125
+ keyword_args: dict[str, Any] | None = None,
126
+ ) -> dict[str, Any]:
127
+ module = self._loaded_modules.get(module_name)
128
+ if module is None:
129
+ return {"status": "error", "error": f"Module not loaded: {module_name}"}
130
 
131
+ if not hasattr(module, function_name):
132
+ return {"status": "error", "error": f"Function not found: {module_name}.{function_name}"}
 
 
 
 
 
 
133
 
134
+ target = getattr(module, function_name)
135
+ if not callable(target):
136
+ return {"status": "error", "error": f"Target is not callable: {module_name}.{function_name}"}
 
 
 
 
137
 
138
+ args = positional_args if positional_args is not None else []
139
+ kwargs = keyword_args if keyword_args is not None else {}
 
140
 
 
 
 
 
 
 
141
  try:
142
+ result = target(*args, **kwargs)
143
+ return {
144
+ "status": "ok",
145
+ "module": module_name,
146
+ "function": function_name,
147
+ "result": result,
148
+ }
 
 
 
149
  except Exception as exc:
150
+ return {
151
+ "status": "error",
152
+ "module": module_name,
153
+ "function": function_name,
154
+ "error": str(exc),
155
+ }
156
+
157
+ def create_instance(
158
+ self,
159
+ module_name: str,
160
+ class_name: str,
161
+ positional_args: list[Any] | None = None,
162
+ keyword_args: dict[str, Any] | None = None,
163
+ ) -> dict[str, Any]:
164
+ module = self._loaded_modules.get(module_name)
165
+ if module is None:
166
+ return {"status": "error", "error": f"Module not loaded: {module_name}"}
167
+
168
+ if not hasattr(module, class_name):
169
+ return {"status": "error", "error": f"Class not found: {module_name}.{class_name}"}
170
+
171
+ target = getattr(module, class_name)
172
+ if not inspect.isclass(target):
173
+ return {"status": "error", "error": f"Target is not a class: {module_name}.{class_name}"}
174
+
175
+ args = positional_args if positional_args is not None else []
176
+ kwargs = keyword_args if keyword_args is not None else {}
177
 
 
 
 
 
 
 
 
178
  try:
179
+ instance = target(*args, **kwargs)
180
+ return {
181
+ "status": "ok",
182
+ "module": module_name,
183
+ "class": class_name,
184
+ "instance_type": type(instance).__name__,
185
+ "instance_repr": repr(instance),
186
+ }
 
 
187
  except Exception as exc:
188
+ return {
189
+ "status": "error",
190
+ "module": module_name,
191
+ "class": class_name,
192
+ "error": str(exc),
193
+ }
backtrader/mcp_output/mcp_plugin/main.py CHANGED
@@ -1,13 +1,9 @@
1
- """
2
- MCP Service Auto-Wrapper - Auto-generated
3
- """
4
  from mcp_service import create_app
5
 
6
- def main():
7
- """Main entry point"""
8
- app = create_app()
9
- return app
10
 
11
  if __name__ == "__main__":
12
- app = main()
13
- app.run()
 
1
+ from __future__ import annotations
2
+
3
+ # Local stdio entry point only (Claude Desktop / CLI); not for web or Docker deployment.
4
  from mcp_service import create_app
5
 
 
 
 
 
6
 
7
  if __name__ == "__main__":
8
+ app = create_app()
9
+ app.run(transport="stdio")
backtrader/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,303 +1,347 @@
1
- import os
 
 
 
2
  import sys
 
 
3
 
4
- source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
5
- if source_path not in sys.path:
6
- sys.path.insert(0, source_path)
7
 
8
- from fastmcp import FastMCP
 
9
 
10
- from tools.rewrite-data import parse_args, runstrat
11
- from tools.yahoodownload import parse_args, YahooDownload
12
- from contrib.utils.iqfeed-to-influxdb import IQFeedTool
13
- from contrib.utils.influxdb-import import InfluxDBTool
14
- from contrib.samples.pair-trading import runstrategy, parse_args, PairTradingStrategy
15
- from samples.weekdays-filler.weekdaysfiller import WeekDaysFiller
16
- from samples.weekdays-filler.weekdaysaligner import parse_args, runstrat
17
 
18
- mcp = FastMCP("unknown_service")
 
 
 
19
 
 
 
 
 
20
 
21
- @mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
22
- def parse_args(payload: dict):
23
- try:
24
- if parse_args is None:
25
- return {"success": False, "result": None, "error": "Function parse_args is not available"}
26
- result = parse_args(**payload)
27
- return {"success": True, "result": result, "error": None}
28
- except Exception as e:
29
- return {"success": False, "result": None, "error": str(e)}
30
-
31
- @mcp.tool(name="runstrat", description="Auto-wrapped function runstrat")
32
- def runstrat(payload: dict):
33
- try:
34
- if runstrat is None:
35
- return {"success": False, "result": None, "error": "Function runstrat is not available"}
36
- result = runstrat(**payload)
37
- return {"success": True, "result": result, "error": None}
38
- except Exception as e:
39
- return {"success": False, "result": None, "error": str(e)}
40
-
41
- @mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
42
- def parse_args(payload: dict):
43
- try:
44
- if parse_args is None:
45
- return {"success": False, "result": None, "error": "Function parse_args is not available"}
46
- result = parse_args(**payload)
47
- return {"success": True, "result": result, "error": None}
48
- except Exception as e:
49
- return {"success": False, "result": None, "error": str(e)}
50
-
51
- @mcp.tool(name="yahoodownload", description="YahooDownload class")
52
- def yahoodownload(*args, **kwargs):
53
- """YahooDownload class"""
54
- try:
55
- if YahooDownload is None:
56
- return {"success": False, "result": None, "error": "Class YahooDownload is not available, path may need adjustment"}
57
-
58
- # MCP parameter type conversion
59
- converted_args = []
60
- converted_kwargs = kwargs.copy()
61
-
62
- # Handle position argument type conversion
63
- for arg in args:
64
- if isinstance(arg, str):
65
- # Try to convert to numeric type
66
- try:
67
- if '.' in arg:
68
- converted_args.append(float(arg))
69
- else:
70
- converted_args.append(int(arg))
71
- except ValueError:
72
- converted_args.append(arg)
73
- else:
74
- converted_args.append(arg)
75
-
76
- # Handle keyword argument type conversion
77
- for key, value in converted_kwargs.items():
78
- if isinstance(value, str):
79
- try:
80
- if '.' in value:
81
- converted_kwargs[key] = float(value)
82
- else:
83
- converted_kwargs[key] = int(value)
84
- except ValueError:
85
- pass
86
-
87
- instance = YahooDownload(*converted_args, **converted_kwargs)
88
- return {"success": True, "result": str(instance), "error": None}
89
- except Exception as e:
90
- return {"success": False, "result": None, "error": str(e)}
91
-
92
- @mcp.tool(name="iqfeedtool", description="IQFeedTool class")
93
- def iqfeedtool(*args, **kwargs):
94
- """IQFeedTool class"""
95
- try:
96
- if IQFeedTool is None:
97
- return {"success": False, "result": None, "error": "Class IQFeedTool is not available, path may need adjustment"}
98
-
99
- # MCP parameter type conversion
100
- converted_args = []
101
- converted_kwargs = kwargs.copy()
102
-
103
- # Handle position argument type conversion
104
- for arg in args:
105
- if isinstance(arg, str):
106
- # Try to convert to numeric type
107
- try:
108
- if '.' in arg:
109
- converted_args.append(float(arg))
110
- else:
111
- converted_args.append(int(arg))
112
- except ValueError:
113
- converted_args.append(arg)
114
- else:
115
- converted_args.append(arg)
116
-
117
- # Handle keyword argument type conversion
118
- for key, value in converted_kwargs.items():
119
- if isinstance(value, str):
120
- try:
121
- if '.' in value:
122
- converted_kwargs[key] = float(value)
123
- else:
124
- converted_kwargs[key] = int(value)
125
- except ValueError:
126
- pass
127
-
128
- instance = IQFeedTool(*converted_args, **converted_kwargs)
129
- return {"success": True, "result": str(instance), "error": None}
130
- except Exception as e:
131
- return {"success": False, "result": None, "error": str(e)}
132
-
133
- @mcp.tool(name="influxdbtool", description="InfluxDBTool class")
134
- def influxdbtool(*args, **kwargs):
135
- """InfluxDBTool class"""
136
- try:
137
- if InfluxDBTool is None:
138
- return {"success": False, "result": None, "error": "Class InfluxDBTool is not available, path may need adjustment"}
139
-
140
- # MCP parameter type conversion
141
- converted_args = []
142
- converted_kwargs = kwargs.copy()
143
-
144
- # Handle position argument type conversion
145
- for arg in args:
146
- if isinstance(arg, str):
147
- # Try to convert to numeric type
148
- try:
149
- if '.' in arg:
150
- converted_args.append(float(arg))
151
- else:
152
- converted_args.append(int(arg))
153
- except ValueError:
154
- converted_args.append(arg)
155
- else:
156
- converted_args.append(arg)
157
-
158
- # Handle keyword argument type conversion
159
- for key, value in converted_kwargs.items():
160
- if isinstance(value, str):
161
- try:
162
- if '.' in value:
163
- converted_kwargs[key] = float(value)
164
- else:
165
- converted_kwargs[key] = int(value)
166
- except ValueError:
167
- pass
168
-
169
- instance = InfluxDBTool(*converted_args, **converted_kwargs)
170
- return {"success": True, "result": str(instance), "error": None}
171
- except Exception as e:
172
- return {"success": False, "result": None, "error": str(e)}
173
-
174
- @mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
175
- def parse_args(payload: dict):
176
- try:
177
- if parse_args is None:
178
- return {"success": False, "result": None, "error": "Function parse_args is not available"}
179
- result = parse_args(**payload)
180
- return {"success": True, "result": result, "error": None}
181
- except Exception as e:
182
- return {"success": False, "result": None, "error": str(e)}
183
-
184
- @mcp.tool(name="runstrategy", description="Auto-wrapped function runstrategy")
185
- def runstrategy(payload: dict):
186
  try:
187
- if runstrategy is None:
188
- return {"success": False, "result": None, "error": "Function runstrategy is not available"}
189
- result = runstrategy(**payload)
190
- return {"success": True, "result": result, "error": None}
191
- except Exception as e:
192
- return {"success": False, "result": None, "error": str(e)}
193
-
194
- @mcp.tool(name="pairtradingstrategy", description="PairTradingStrategy class")
195
- def pairtradingstrategy(*args, **kwargs):
196
- """PairTradingStrategy class"""
 
 
 
 
 
 
 
 
 
 
 
197
  try:
198
- if PairTradingStrategy is None:
199
- return {"success": False, "result": None, "error": "Class PairTradingStrategy is not available, path may need adjustment"}
200
-
201
- # MCP parameter type conversion
202
- converted_args = []
203
- converted_kwargs = kwargs.copy()
204
-
205
- # Handle position argument type conversion
206
- for arg in args:
207
- if isinstance(arg, str):
208
- # Try to convert to numeric type
209
- try:
210
- if '.' in arg:
211
- converted_args.append(float(arg))
212
- else:
213
- converted_args.append(int(arg))
214
- except ValueError:
215
- converted_args.append(arg)
216
- else:
217
- converted_args.append(arg)
218
-
219
- # Handle keyword argument type conversion
220
- for key, value in converted_kwargs.items():
221
- if isinstance(value, str):
222
- try:
223
- if '.' in value:
224
- converted_kwargs[key] = float(value)
225
- else:
226
- converted_kwargs[key] = int(value)
227
- except ValueError:
228
- pass
229
-
230
- instance = PairTradingStrategy(*converted_args, **converted_kwargs)
231
- return {"success": True, "result": str(instance), "error": None}
232
- except Exception as e:
233
- return {"success": False, "result": None, "error": str(e)}
234
-
235
- @mcp.tool(name="weekdaysfiller", description="WeekDaysFiller class")
236
- def weekdaysfiller(*args, **kwargs):
237
- """WeekDaysFiller class"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  try:
239
- if WeekDaysFiller is None:
240
- return {"success": False, "result": None, "error": "Class WeekDaysFiller is not available, path may need adjustment"}
241
-
242
- # MCP parameter type conversion
243
- converted_args = []
244
- converted_kwargs = kwargs.copy()
245
-
246
- # Handle position argument type conversion
247
- for arg in args:
248
- if isinstance(arg, str):
249
- # Try to convert to numeric type
250
- try:
251
- if '.' in arg:
252
- converted_args.append(float(arg))
253
- else:
254
- converted_args.append(int(arg))
255
- except ValueError:
256
- converted_args.append(arg)
257
- else:
258
- converted_args.append(arg)
259
-
260
- # Handle keyword argument type conversion
261
- for key, value in converted_kwargs.items():
262
- if isinstance(value, str):
263
- try:
264
- if '.' in value:
265
- converted_kwargs[key] = float(value)
266
- else:
267
- converted_kwargs[key] = int(value)
268
- except ValueError:
269
- pass
270
-
271
- instance = WeekDaysFiller(*converted_args, **converted_kwargs)
272
- return {"success": True, "result": str(instance), "error": None}
273
- except Exception as e:
274
- return {"success": False, "result": None, "error": str(e)}
275
-
276
- @mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
277
- def parse_args(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  try:
279
- if parse_args is None:
280
- return {"success": False, "result": None, "error": "Function parse_args is not available"}
281
- result = parse_args(**payload)
282
- return {"success": True, "result": result, "error": None}
283
- except Exception as e:
284
- return {"success": False, "result": None, "error": str(e)}
285
-
286
- @mcp.tool(name="runstrat", description="Auto-wrapped function runstrat")
287
- def runstrat(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  try:
289
- if runstrat is None:
290
- return {"success": False, "result": None, "error": "Function runstrat is not available"}
291
- result = runstrat(**payload)
292
- return {"success": True, "result": result, "error": None}
293
- except Exception as e:
294
- return {"success": False, "result": None, "error": str(e)}
 
 
 
295
 
 
 
 
 
 
 
296
 
 
 
297
 
298
- def create_app():
299
- """Create and return FastMCP application instance"""
300
  return mcp
301
 
 
302
  if __name__ == "__main__":
303
- mcp.run(transport="http", host="0.0.0.0", port=8000)
 
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import json
5
  import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
 
9
+ _PLUGIN_DIR = Path(__file__).resolve().parent
10
+ _REPO_DIR = _PLUGIN_DIR.parent.parent
11
+ _SOURCE_DIR = _REPO_DIR / "source"
12
 
13
+ if str(_SOURCE_DIR) not in sys.path:
14
+ sys.path.insert(0, str(_SOURCE_DIR))
15
 
16
+ try:
17
+ from fastmcp import FastMCP
18
+ except Exception:
19
+ FastMCP = None
 
 
 
20
 
21
+ try:
22
+ import backtrader as bt
23
+ except Exception:
24
+ bt = None
25
 
26
+ try:
27
+ import backtrader.feeds as btfeeds
28
+ except Exception:
29
+ btfeeds = None
30
 
31
+ try:
32
+ import backtrader.indicators as btind
33
+ except Exception:
34
+ btind = None
35
+
36
+ try:
37
+ import backtrader.analyzers as btanalyzers
38
+ except Exception:
39
+ btanalyzers = None
40
+
41
+ try:
42
+ import backtrader.sizers as btsizers
43
+ except Exception:
44
+ btsizers = None
45
+
46
+ try:
47
+ from adapter import Adapter
48
+ except Exception:
49
+ Adapter = None
50
+
51
+
52
+ class _FallbackMCP:
53
+ def __init__(self, name: str) -> None:
54
+ self.name = name
55
+ self.tools: list[Any] = []
56
+
57
+ def tool(self, name: str, description: str):
58
+ def decorator(func):
59
+ setattr(func, "name", name)
60
+ setattr(func, "description", description)
61
+ self.tools.append(func)
62
+ return func
63
+
64
+ return decorator
65
+
66
+ def run(self, transport: str | None = None, host: str | None = None, port: int | None = None) -> None:
67
+ raise RuntimeError(
68
+ "fastmcp is not available. Install dependencies and retry. "
69
+ f"Requested transport={transport}, host={host}, port={port}"
70
+ )
71
+
72
+
73
+ mcp = FastMCP("backtrader-mcp") if FastMCP is not None else _FallbackMCP("backtrader-mcp")
74
+ _ADAPTER = Adapter("backtrader") if Adapter is not None else None
75
+
76
+
77
+ def _response(success: bool, result: Any = None, error: str | None = None) -> dict[str, Any]:
78
+ return {"success": success, "result": result, "error": error}
79
+
80
+
81
+ def _resolve_data_path(data_path: str) -> Path:
82
+ candidate = Path(data_path)
83
+ if candidate.is_absolute() and candidate.exists():
84
+ return candidate
85
+
86
+ local_candidate = _REPO_DIR / data_path
87
+ if local_candidate.exists():
88
+ return local_candidate
89
+
90
+ sample_candidate = _SOURCE_DIR.parent / "datas" / data_path
91
+ if sample_candidate.exists():
92
+ return sample_candidate
93
+
94
+ return candidate
95
+
96
+
97
+ @mcp.tool(name="health_check", description="检查 MCP 服务和关键依赖的可用性")
98
+ def health_check() -> dict[str, Any]:
99
+ """返回服务健康状态及依赖加载情况。"""
100
+ dependencies = {
101
+ "fastmcp": FastMCP is not None,
102
+ "backtrader": bt is not None,
103
+ "backtrader.feeds": btfeeds is not None,
104
+ "backtrader.indicators": btind is not None,
105
+ "backtrader.analyzers": btanalyzers is not None,
106
+ "backtrader.sizers": btsizers is not None,
107
+ "adapter": _ADAPTER is not None,
108
+ "source_dir_exists": _SOURCE_DIR.exists(),
109
+ }
110
+
111
+ adapter_health = _ADAPTER.health() if _ADAPTER is not None else {"status": "fallback", "error": "adapter unavailable"}
112
+ return _response(True, {"dependencies": dependencies, "adapter": adapter_health}, None)
113
+
114
+
115
+ @mcp.tool(name="list_modules", description="列出 backtrader 已加载与加载失败的模块")
116
+ def list_modules() -> dict[str, Any]:
117
+ """列出适配器当前可见的模块集合。"""
118
+ if _ADAPTER is None:
119
+ return _response(False, None, "adapter unavailable")
120
+
121
+ result = _ADAPTER.list_modules()
122
+ ok = result.get("status") in {"ok", "fallback"}
123
+ return _response(ok, result, None if ok else result.get("error", "list_modules failed"))
124
+
125
+
126
+ @mcp.tool(name="list_symbols", description="查看指定模块的符号清单")
127
+ def list_symbols(module_name: str, include_private: bool = False, limit: int = 200) -> dict[str, Any]:
128
+ """参数:
129
+ - module_name: 目标模块全名,例如 backtrader.indicators
130
+ - include_private: 是否包含以下划线开头符号
131
+ - limit: 返回符号数量上限
132
+ """
133
+ if _ADAPTER is None:
134
+ return _response(False, None, "adapter unavailable")
135
+
136
+ result = _ADAPTER.list_symbols(module_name=module_name, include_private=include_private, limit=limit)
137
+ ok = result.get("status") == "ok"
138
+ return _response(ok, result if ok else None, None if ok else result.get("error", "list_symbols failed"))
139
+
140
+
141
+ @mcp.tool(name="preview_csv_data", description="预览本地 CSV 行情数据样本")
142
+ def preview_csv_data(data_path: str, delimiter: str = ",", max_rows: int = 5) -> dict[str, Any]:
143
+ """参数:
144
+ - data_path: CSV 文件路径,可为绝对路径或相对路径
145
+ - delimiter: 分隔符,默认逗号
146
+ - max_rows: 最多返回行数
147
+ """
148
+ resolved = _resolve_data_path(data_path)
149
+ if not resolved.exists():
150
+ return _response(False, None, f"data file not found: {resolved}")
151
+
152
+ rows: list[list[str]] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  try:
154
+ with resolved.open("r", encoding="utf-8", newline="") as handle:
155
+ reader = csv.reader(handle, delimiter=delimiter)
156
+ for index, row in enumerate(reader):
157
+ rows.append(row)
158
+ if index + 1 >= max(max_rows, 1):
159
+ break
160
+ except Exception as exc:
161
+ return _response(False, None, str(exc))
162
+
163
+ return _response(True, {"path": str(resolved), "rows": rows}, None)
164
+
165
+
166
+ @mcp.tool(name="create_cerebro_session", description="创建并摘要一个基础 Cerebro 回测会话")
167
+ def create_cerebro_session(initial_cash: float = 10000.0, commission: float = 0.001) -> dict[str, Any]:
168
+ """参数:
169
+ - initial_cash: 初始资金
170
+ - commission: 佣金比例
171
+ """
172
+ if bt is None:
173
+ return _response(False, None, "backtrader not available")
174
+
175
  try:
176
+ cerebro = bt.Cerebro()
177
+ cerebro.broker.setcash(float(initial_cash))
178
+ cerebro.broker.setcommission(commission=float(commission))
179
+
180
+ result = {
181
+ "cash": cerebro.broker.getcash(),
182
+ "commission": commission,
183
+ "strategy_count": len(getattr(cerebro, "strats", [])),
184
+ "data_count": len(getattr(cerebro, "datas", [])),
185
+ }
186
+ return _response(True, result, None)
187
+ except Exception as exc:
188
+ return _response(False, None, str(exc))
189
+
190
+
191
+ @mcp.tool(name="run_sma_crossover_backtest", description="运行简单均线交叉策略回测")
192
+ def run_sma_crossover_backtest(
193
+ data_path: str,
194
+ fast_period: int = 10,
195
+ slow_period: int = 30,
196
+ initial_cash: float = 10000.0,
197
+ commission: float = 0.001,
198
+ stake: int = 1,
199
+ ) -> dict[str, Any]:
200
+ """参数:
201
+ - data_path: CSV 数据路径,需包含 OHLCV
202
+ - fast_period: 快均线周期
203
+ - slow_period: 慢均线周期
204
+ - initial_cash: 初始资金
205
+ - commission: 佣金比例
206
+ - stake: 每次交易手数
207
+ """
208
+ if bt is None or btfeeds is None:
209
+ return _response(False, None, "backtrader dependencies are unavailable")
210
+
211
+ if fast_period <= 0 or slow_period <= 0:
212
+ return _response(False, None, "period must be positive")
213
+
214
+ if fast_period >= slow_period:
215
+ return _response(False, None, "fast_period must be smaller than slow_period")
216
+
217
+ resolved = _resolve_data_path(data_path)
218
+ if not resolved.exists():
219
+ return _response(False, None, f"data file not found: {resolved}")
220
+
221
+ class SmaCrossStrategy(bt.Strategy):
222
+ params = (("fast_period", 10), ("slow_period", 30), ("stake", 1))
223
+
224
+ def __init__(self) -> None:
225
+ sma_fast = bt.indicators.SMA(self.datas[0].close, period=int(self.p.fast_period))
226
+ sma_slow = bt.indicators.SMA(self.datas[0].close, period=int(self.p.slow_period))
227
+ self.crossover = bt.indicators.CrossOver(sma_fast, sma_slow)
228
+
229
+ def next(self) -> None:
230
+ if not self.position and self.crossover > 0:
231
+ self.buy(size=int(self.p.stake))
232
+ elif self.position and self.crossover < 0:
233
+ self.close()
234
+
235
  try:
236
+ data_feed = btfeeds.GenericCSVData(
237
+ dataname=str(resolved),
238
+ dtformat="%Y-%m-%d",
239
+ datetime=0,
240
+ open=1,
241
+ high=2,
242
+ low=3,
243
+ close=4,
244
+ volume=5,
245
+ openinterest=-1,
246
+ headers=False,
247
+ )
248
+
249
+ cerebro = bt.Cerebro()
250
+ cerebro.adddata(data_feed)
251
+ cerebro.addstrategy(
252
+ SmaCrossStrategy,
253
+ fast_period=int(fast_period),
254
+ slow_period=int(slow_period),
255
+ stake=int(stake),
256
+ )
257
+ cerebro.broker.setcash(float(initial_cash))
258
+ cerebro.broker.setcommission(commission=float(commission))
259
+
260
+ start_value = cerebro.broker.getvalue()
261
+ cerebro.run()
262
+ end_value = cerebro.broker.getvalue()
263
+
264
+ result = {
265
+ "data_path": str(resolved),
266
+ "start_value": start_value,
267
+ "end_value": end_value,
268
+ "pnl": end_value - start_value,
269
+ "fast_period": fast_period,
270
+ "slow_period": slow_period,
271
+ }
272
+ return _response(True, result, None)
273
+ except Exception as exc:
274
+ return _response(False, None, str(exc))
275
+
276
+
277
+ @mcp.tool(name="list_builtin_components", description="列出内置指标/分析器/仓位管理组件")
278
+ def list_builtin_components(component_type: str = "indicators", limit: int = 50) -> dict[str, Any]:
279
+ """参数:
280
+ - component_type: 可选 indicators / analyzers / sizers
281
+ - limit: 返回数量上限
282
+ """
283
+ if limit <= 0:
284
+ return _response(False, None, "limit must be positive")
285
+
286
+ mapping: dict[str, Any] = {
287
+ "indicators": btind,
288
+ "analyzers": btanalyzers,
289
+ "sizers": btsizers,
290
+ }
291
+
292
+ module = mapping.get(component_type)
293
+ if module is None:
294
+ return _response(False, None, f"unsupported component_type: {component_type}")
295
+
296
  try:
297
+ names = [name for name in dir(module) if not name.startswith("_")]
298
+ sample = names[:limit]
299
+ return _response(True, {"component_type": component_type, "count": len(sample), "items": sample}, None)
300
+ except Exception as exc:
301
+ return _response(False, None, str(exc))
302
+
303
+
304
+ @mcp.tool(name="adapter_call_function", description="通过适配器调用已加载模块中的函数")
305
+ def adapter_call_function(
306
+ module_name: str,
307
+ function_name: str,
308
+ positional_args_json: str = "[]",
309
+ keyword_args_json: str = "{}",
310
+ ) -> dict[str, Any]:
311
+ """参数:
312
+ - module_name: 模块名
313
+ - function_name: 函数名
314
+ - positional_args_json: JSON 数组字符串
315
+ - keyword_args_json: JSON 对象字符串
316
+ """
317
+ if _ADAPTER is None:
318
+ return _response(False, None, "adapter unavailable")
319
+
320
  try:
321
+ positional_args = json.loads(positional_args_json)
322
+ keyword_args = json.loads(keyword_args_json)
323
+ except Exception as exc:
324
+ return _response(False, None, f"invalid JSON args: {exc}")
325
+
326
+ if not isinstance(positional_args, list):
327
+ return _response(False, None, "positional_args_json must decode to a list")
328
+ if not isinstance(keyword_args, dict):
329
+ return _response(False, None, "keyword_args_json must decode to an object")
330
 
331
+ result = _ADAPTER.call_function(
332
+ module_name=module_name,
333
+ function_name=function_name,
334
+ positional_args=positional_args,
335
+ keyword_args=keyword_args,
336
+ )
337
 
338
+ ok = result.get("status") == "ok"
339
+ return _response(ok, result if ok else None, None if ok else result.get("error", "adapter call failed"))
340
 
341
+
342
+ def create_app() -> Any:
343
  return mcp
344
 
345
+
346
  if __name__ == "__main__":
347
+ mcp.run()
backtrader/mcp_output/requirements.txt CHANGED
@@ -1,6 +1,2 @@
1
  fastmcp
2
- fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- six
6
- matplotlib
 
1
  fastmcp
2
+ backtrader
 
 
 
 
backtrader/mcp_output/start_mcp.py CHANGED
@@ -1,30 +1,35 @@
 
1
 
2
- """
3
- MCP Service Startup Entry
4
- """
5
- import sys
6
  import os
 
 
 
 
 
7
 
8
- project_root = os.path.dirname(os.path.abspath(__file__))
9
- mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
- if mcp_plugin_dir not in sys.path:
11
- sys.path.insert(0, mcp_plugin_dir)
12
 
13
  from mcp_service import create_app
14
 
15
- def main():
16
- """Start FastMCP service"""
 
 
 
 
 
 
 
 
17
  app = create_app()
18
- # Use environment variable to configure port, default 8000
19
- port = int(os.environ.get("MCP_PORT", "8000"))
20
-
21
- # Choose transport mode based on environment variable
22
- transport = os.environ.get("MCP_TRANSPORT", "stdio")
23
  if transport == "http":
24
  app.run(transport="http", host="0.0.0.0", port=port)
25
- else:
26
- # Default to STDIO mode
27
- app.run()
 
28
 
29
  if __name__ == "__main__":
30
  main()
 
1
+ from __future__ import annotations
2
 
 
 
 
 
3
  import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ CURRENT_DIR = Path(__file__).resolve().parent
8
+ PLUGIN_DIR = CURRENT_DIR / "mcp_plugin"
9
 
10
+ if str(PLUGIN_DIR) not in sys.path:
11
+ sys.path.insert(0, str(PLUGIN_DIR))
 
 
12
 
13
  from mcp_service import create_app
14
 
15
+
16
+ def main() -> None:
17
+ transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower()
18
+ port_raw = os.getenv("MCP_PORT", "8000").strip()
19
+
20
+ try:
21
+ port = int(port_raw)
22
+ except ValueError:
23
+ port = 8000
24
+
25
  app = create_app()
26
+
 
 
 
 
27
  if transport == "http":
28
  app.run(transport="http", host="0.0.0.0", port=port)
29
+ return
30
+
31
+ app.run(transport="stdio")
32
+
33
 
34
  if __name__ == "__main__":
35
  main()
port.json CHANGED
@@ -1,5 +1 @@
1
- {
2
- "repo": "backtrader",
3
- "port": 7946,
4
- "timestamp": 1773378088
5
- }
 
1
+ {"port": 7860}
 
 
 
 
requirements.txt CHANGED
@@ -1,6 +1,4 @@
1
  fastmcp
 
2
  fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- six
6
- matplotlib
 
1
  fastmcp
2
+ backtrader
3
  fastapi
4
+ uvicorn
 
 
 
run_docker.ps1 CHANGED
@@ -1,26 +1,10 @@
1
- cd $PSScriptRoot
2
  $ErrorActionPreference = "Stop"
3
- $entryName = if ($env:MCP_ENTRY_NAME) { $env:MCP_ENTRY_NAME } else { "backtrader" }
4
- $entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7946/mcp" }
5
- $imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "backtrader-mcp" }
6
- $mcpDir = Join-Path $env:USERPROFILE ".cursor"
7
- $mcpPath = Join-Path $mcpDir "mcp.json"
8
- if (!(Test-Path $mcpDir)) { New-Item -ItemType Directory -Path $mcpDir | Out-Null }
9
- $config = @{}
10
- if (Test-Path $mcpPath) {
11
- try { $config = Get-Content $mcpPath -Raw | ConvertFrom-Json } catch { $config = @{} }
12
- }
13
- $serversOrdered = [ordered]@{}
14
- if ($config -and ($config.PSObject.Properties.Name -contains "mcpServers") -and $config.mcpServers) {
15
- $existing = $config.mcpServers
16
- if ($existing -is [pscustomobject]) {
17
- foreach ($p in $existing.PSObject.Properties) { if ($p.Name -ne $entryName) { $serversOrdered[$p.Name] = $p.Value } }
18
- } elseif ($existing -is [System.Collections.IDictionary]) {
19
- foreach ($k in $existing.Keys) { if ($k -ne $entryName) { $serversOrdered[$k] = $existing[$k] } }
20
- }
21
- }
22
- $serversOrdered[$entryName] = @{ url = $entryUrl }
23
- $config = @{ mcpServers = $serversOrdered }
24
- $config | ConvertTo-Json -Depth 10 | Set-Content -Path $mcpPath -Encoding UTF8
25
- docker build -t $imageName .
26
- docker run --rm -p 7946:7860 $imageName
 
 
1
  $ErrorActionPreference = "Stop"
2
+
3
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
4
+ $PortConfig = Get-Content (Join-Path $ScriptDir "port.json") -Raw | ConvertFrom-Json
5
+ $Port = [int]$PortConfig.port
6
+ $ImageName = "backtrader-mcp"
7
+
8
+ Set-Location $ScriptDir
9
+ docker build -t $ImageName .
10
+ docker run --rm -p "${Port}:7860" $ImageName
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
run_docker.sh CHANGED
@@ -1,75 +1,10 @@
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
- cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4
- mcp_entry_name="${MCP_ENTRY_NAME:-backtrader}"
5
- mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7946/mcp}"
6
- mcp_dir="${HOME}/.cursor"
7
- mcp_path="${mcp_dir}/mcp.json"
8
- mkdir -p "${mcp_dir}"
9
- if command -v python3 >/dev/null 2>&1; then
10
- python3 - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
11
- import json, os, sys
12
- path, name, url = sys.argv[1:4]
13
- cfg = {"mcpServers": {}}
14
- if os.path.exists(path):
15
- try:
16
- with open(path, "r", encoding="utf-8") as f:
17
- cfg = json.load(f)
18
- except Exception:
19
- cfg = {"mcpServers": {}}
20
- if not isinstance(cfg, dict):
21
- cfg = {"mcpServers": {}}
22
- servers = cfg.get("mcpServers")
23
- if not isinstance(servers, dict):
24
- servers = {}
25
- ordered = {}
26
- for k, v in servers.items():
27
- if k != name:
28
- ordered[k] = v
29
- ordered[name] = {"url": url}
30
- cfg = {"mcpServers": ordered}
31
- with open(path, "w", encoding="utf-8") as f:
32
- json.dump(cfg, f, indent=2, ensure_ascii=False)
33
- PY
34
- elif command -v python >/dev/null 2>&1; then
35
- python - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
36
- import json, os, sys
37
- path, name, url = sys.argv[1:4]
38
- cfg = {"mcpServers": {}}
39
- if os.path.exists(path):
40
- try:
41
- with open(path, "r", encoding="utf-8") as f:
42
- cfg = json.load(f)
43
- except Exception:
44
- cfg = {"mcpServers": {}}
45
- if not isinstance(cfg, dict):
46
- cfg = {"mcpServers": {}}
47
- servers = cfg.get("mcpServers")
48
- if not isinstance(servers, dict):
49
- servers = {}
50
- ordered = {}
51
- for k, v in servers.items():
52
- if k != name:
53
- ordered[k] = v
54
- ordered[name] = {"url": url}
55
- cfg = {"mcpServers": ordered}
56
- with open(path, "w", encoding="utf-8") as f:
57
- json.dump(cfg, f, indent=2, ensure_ascii=False)
58
- PY
59
- elif command -v jq >/dev/null 2>&1; then
60
- name="${mcp_entry_name}"; url="${mcp_entry_url}"
61
- if [ -f "${mcp_path}" ]; then
62
- tmp="$(mktemp)"
63
- jq --arg name "$name" --arg url "$url" '
64
- .mcpServers = (.mcpServers // {})
65
- | .mcpServers as $s
66
- | ($s | with_entries(select(.key != $name))) as $base
67
- | .mcpServers = ($base + {($name): {"url": $url}})
68
- ' "${mcp_path}" > "${tmp}" && mv "${tmp}" "${mcp_path}"
69
- else
70
- printf '{ "mcpServers": { "%s": { "url": "%s" } } }
71
- ' "$name" "$url" > "${mcp_path}"
72
- fi
73
- fi
74
- docker build -t backtrader-mcp .
75
- docker run --rm -p 7946:7860 backtrader-mcp
 
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PORT="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["port"])' "$SCRIPT_DIR/port.json")"
6
+ IMAGE_NAME="backtrader-mcp"
7
+
8
+ cd "$SCRIPT_DIR"
9
+ docker build -t "$IMAGE_NAME" .
10
+ docker run --rm -p "${PORT}:7860" "$IMAGE_NAME"