binance-quant-engine 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ """Host interface for execution mixins.
2
+
3
+ The live trading bot is composed from mixins (``BracketMixin`` etc.) that are
4
+ mixed into a single ``Scalper`` class. Each mixin needs to call methods that
5
+ live on *sibling* mixins or on the composed host. To type-check a mixin in
6
+ isolation — without importing the concrete (and strategy-specific) host class —
7
+ we declare the surface it relies on as a ``Protocol``.
8
+
9
+ This is the strategy-agnostic slice of that interface: only the order-placement
10
+ and bookkeeping members the bracket manager actually touches. Concrete trailing
11
+ parameters come from :class:`TrailConfig` rather than any proprietary strategy
12
+ config, so the bracket logic is fully decoupled from alpha.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import TYPE_CHECKING, Any, Protocol
19
+
20
+ if TYPE_CHECKING: # pragma: no cover
21
+ from binance_quant_engine.strategy.protocol import TradingStrategy
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class TrailConfig:
26
+ """Trailing-stop geometry, expressed as fractions of entry price.
27
+
28
+ Attributes:
29
+ trail_distance: Callback distance the stop trails behind the peak
30
+ (e.g. ``0.005`` = 0.5%). Binance requires callbackRate >= 0.5%.
31
+ trail_activate: Unrealised gain at which the trailing stop arms
32
+ (e.g. ``0.02`` = +2%).
33
+ """
34
+
35
+ trail_distance: float = 0.005
36
+ trail_activate: float = 0.02
37
+
38
+
39
+ class ScalperProtocol(Protocol):
40
+ """Host surface used by :class:`BracketMixin` (strategy-agnostic)."""
41
+
42
+ # ── state ────────────────────────────────────────────────────────
43
+ paper_mode: bool
44
+ client: Any
45
+ discord: Any
46
+ strategy: "TradingStrategy"
47
+ trail_config: TrailConfig
48
+ _algo_api: Any
49
+
50
+ # ── exchange helpers (provided by other mixins) ──────────────────
51
+ def get_symbol_info(self, symbol: str) -> dict[str, Any]: ...
52
+ def _round_qty(self, qty: float, symbol: str) -> float: ...
53
+ def _save_state(self) -> None: ...
54
+
55
+ def _place_algo_order(self, *args: Any, **kwargs: Any) -> Any: ...
56
+ def _cancel_server_order(self, symbol: str, order_id: str, label: str = "order") -> bool: ...
57
+ def _cancel_server_stop_loss(self, symbol: str, order_id: str) -> bool: ...
58
+ def _verify_algo_order_active(self, algo_id: str, symbol: str) -> str | None: ...
59
+ def _place_trailing_stop_market(self, *args: Any, **kwargs: Any) -> Any: ...
60
+ def _cancel_trailing_stop_market(self, symbol: str, order_id: str) -> bool: ...
61
+ def _check_trail_order_status(self, symbol: str, order_id: str) -> dict[str, Any] | None: ...
62
+ def _check_order_filled(self, symbol: str, order_id: str) -> dict[str, Any] | None: ...
63
+ def _lookup_actual_fill(
64
+ self, symbol: str, expected_qty: str, order_type: str
65
+ ) -> dict[str, Any] | None: ...
@@ -0,0 +1,46 @@
1
+ """Small, strategy-agnostic helpers shared across the execution layer.
2
+
3
+ Kept separate from :mod:`brackets` so they can be unit-tested (and reused by
4
+ a host application) without pulling in the full bracket-order state machine.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ #: Binance algo/conditional-order statuses that mean the order is no longer
12
+ #: live and bookkeeping can stop tracking it. Centralized here so a status
13
+ #: string Binance adds later only needs to be taught to one place.
14
+ #:
15
+ #: Includes both the US and UK spellings of "cancelled" — Binance's own API
16
+ #: is inconsistent about which one a given endpoint returns.
17
+ ALGO_ORDER_DEAD_STATUSES = frozenset(
18
+ {"CANCELLED", "CANCELED", "EXPIRED", "USER_CANCELLED", "ERROR", "REJECTED"}
19
+ )
20
+
21
+
22
+ def round_to_tick(price: float, tick_size: float, price_precision: int) -> float:
23
+ """Snap *price* to the symbol's tick grid.
24
+
25
+ Binance rejects (or silently misprices) an order whose price isn't an
26
+ exact multiple of the symbol's ``tickSize`` — a raw float division
27
+ (``price / tick_size``) almost never lands exactly on the grid because of
28
+ binary floating-point rounding. Round to the nearest tick first, then to
29
+ the symbol's quoted decimal precision, in that order.
30
+ """
31
+ if tick_size <= 0:
32
+ return round(price, price_precision)
33
+ return round(round(price / tick_size) * tick_size, price_precision)
34
+
35
+
36
+ def to_api_symbol(symbol: str, position: dict[str, Any] | None = None) -> str:
37
+ """Strip a colon-suffixed compound key down to the bare Binance API symbol.
38
+
39
+ Two unrelated conventions both tack a suffix onto the symbol with ``:``,
40
+ and both need the same fix before a raw Binance REST call: a CCXT-style
41
+ ``BASE/QUOTE:SETTLE`` key (e.g. ``"BTC/USDT:USDT"``), and a Hedge Mode
42
+ position key (e.g. ``"BTCUSDT:LONG"``). *position*, if given, may carry an
43
+ already-resolved ``symbol`` field that takes precedence over *symbol*.
44
+ """
45
+ raw = (position or {}).get("symbol", symbol)
46
+ return raw.split(":")[0] if ":" in raw else raw
@@ -0,0 +1 @@
1
+ """Optional MCP (Model Context Protocol) server — install with the ``mcp`` extra."""
@@ -0,0 +1,115 @@
1
+ """MCP stdio server exposing the backtest engine to any MCP-compatible agent.
2
+
3
+ Deliberately backtest-only. This process never touches an exchange account —
4
+ there is no tool here that can place, modify, or cancel a live order. An
5
+ agent can explore the engine's demo strategy, run its own parameter sweeps,
6
+ and backtest its own OHLC data, but "let an LLM trade my account" is not a
7
+ capability this server offers. Live execution (:mod:`binance_quant_engine.execution`)
8
+ is a library you wire into your own bot; it is intentionally not MCP-reachable.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import csv
14
+ import io
15
+ import math
16
+ from dataclasses import replace
17
+ from typing import Any
18
+
19
+ from mcp.server.mcpserver import MCPServer
20
+
21
+ from binance_quant_engine.backtest.vectorized import run_backtest
22
+ from binance_quant_engine.data.klines import synth_ohlcv
23
+ from binance_quant_engine.strategy.demo_squeeze import SqueezeConfig, SqueezeStrategy
24
+
25
+ mcp = MCPServer("binance-quant-engine")
26
+
27
+
28
+ def _finite(x: float) -> float:
29
+ """JSON has no representation for inf/nan; MCP transport is JSON."""
30
+ return x if math.isfinite(x) else 0.0
31
+
32
+
33
+ def _summary_json(result: Any) -> dict[str, float]:
34
+ return {k: _finite(float(v)) for k, v in result.summary().items()}
35
+
36
+
37
+ @mcp.tool()
38
+ def run_demo_backtest(
39
+ n_bars: int = 2000,
40
+ seed: int = 7,
41
+ bb_period: int = 20,
42
+ bb_std: float = 2.0,
43
+ squeeze_pct: float = 0.25,
44
+ stop_loss: float = 0.02,
45
+ take_profit: float = 0.04,
46
+ ) -> dict[str, Any]:
47
+ """Backtest the bundled Bollinger-squeeze demo strategy on synthetic OHLC.
48
+
49
+ Zero setup, no API keys, no data file — useful for confirming the engine
50
+ works and for exploring how the demo strategy's parameters move its
51
+ metrics. Not a real alpha; see the README before reading any number here
52
+ as investment advice.
53
+ """
54
+ high, low, close = synth_ohlcv(n=n_bars, seed=seed)
55
+ config = SqueezeConfig(
56
+ bb_period=bb_period,
57
+ bb_std=bb_std,
58
+ squeeze_pct=squeeze_pct,
59
+ stop_loss=stop_loss,
60
+ take_profit=take_profit,
61
+ )
62
+ result = run_backtest(SqueezeStrategy(config), high, low, close)
63
+ return {"summary": _summary_json(result), "config": config.__dict__}
64
+
65
+
66
+ @mcp.tool()
67
+ def backtest_csv(csv_text: str, config_overrides: dict[str, float] | None = None) -> dict[str, Any]:
68
+ """Backtest the demo squeeze strategy against caller-supplied OHLC data.
69
+
70
+ Args:
71
+ csv_text: CSV content (not a path — this process has no filesystem
72
+ access to the caller's machine) with ``high``, ``low``, ``close``
73
+ columns, case-insensitive.
74
+ config_overrides: Optional subset of :class:`SqueezeConfig` fields to
75
+ override (e.g. ``{"stop_loss": 0.015}``).
76
+ """
77
+ reader = csv.DictReader(io.StringIO(csv_text))
78
+ rows = list(reader)
79
+ if not rows:
80
+ raise ValueError("csv_text has no data rows")
81
+ cols = {c.lower(): c for c in rows[0]}
82
+ for required in ("high", "low", "close"):
83
+ if required not in cols:
84
+ raise ValueError(f"csv_text is missing a '{required}' column")
85
+
86
+ high = [float(r[cols["high"]]) for r in rows]
87
+ low = [float(r[cols["low"]]) for r in rows]
88
+ close = [float(r[cols["close"]]) for r in rows]
89
+
90
+ config = SqueezeConfig()
91
+ if config_overrides:
92
+ config = replace(config, **config_overrides)
93
+
94
+ result = run_backtest(SqueezeStrategy(config), high, low, close)
95
+ return {"summary": _summary_json(result), "n_bars": len(close), "config": config.__dict__}
96
+
97
+
98
+ @mcp.tool()
99
+ def describe_strategy_protocol() -> str:
100
+ """Return the docstring of the TradingStrategy protocol.
101
+
102
+ Read this before implementing a custom strategy — it's the exact
103
+ interface :func:`run_backtest` (and the live engine) drives.
104
+ """
105
+ from binance_quant_engine.strategy.protocol import TradingStrategy
106
+
107
+ return TradingStrategy.__doc__ or ""
108
+
109
+
110
+ def main() -> None:
111
+ mcp.run()
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
@@ -0,0 +1 @@
1
+ """Trading strategies (interface + reference demo)."""
@@ -0,0 +1,183 @@
1
+ """Demo strategy — Bollinger Band squeeze mean-reversion.
2
+
3
+ A deliberately simple, **public-domain** strategy that exists to exercise the
4
+ engine end to end, not to make money. It implements the
5
+ :class:`~binance_quant_engine.strategy.protocol.TradingStrategy` contract so the backtester
6
+ and the live scalper can both drive it without modification.
7
+
8
+ Idea (textbook):
9
+ * A *squeeze* is a low-volatility regime — Bollinger Band width sits in the
10
+ bottom decile of its recent range. Such compression often precedes
11
+ expansion.
12
+ * During a squeeze we fade extremes: a close below the lower band is a long
13
+ (mean-reversion up), a close above the upper band is a short.
14
+ * Each position carries a fixed stop-loss, take-profit, and an optional
15
+ trailing stop once it moves into profit.
16
+
17
+ Discipline:
18
+ Every indicator reads only *completed* bars (the arrays passed in exclude
19
+ the in-progress candle). No future data touches a signal — see
20
+ :mod:`binance_quant_engine.backtest.vectorized` for how the engine enforces this.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass
26
+
27
+ import numpy as np
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class SqueezeConfig:
32
+ """Strategy parameters (transparent, public defaults)."""
33
+
34
+ bb_period: int = 20 # Bollinger Band lookback
35
+ bb_std: float = 2.0 # band width in standard deviations
36
+ squeeze_lookback: int = 100 # window for the band-width percentile
37
+ squeeze_pct: float = 0.25 # "squeeze" if width <= this percentile
38
+ atr_period: int = 14
39
+ stop_loss: float = 0.02 # 2% hard stop
40
+ take_profit: float = 0.04 # 4% target
41
+ trail_distance: float = 0.01 # trail 1% behind peak once armed
42
+ trail_activate: float = 0.02 # arm the trail at +2% unrealised
43
+
44
+
45
+ def _sma(x: np.ndarray, n: int) -> float:
46
+ return float(np.mean(x[-n:]))
47
+
48
+
49
+ def _atr_pct(high: np.ndarray, low: np.ndarray, close: np.ndarray, n: int) -> float:
50
+ """ATR as a fraction of the last close (Wilder true range, simple mean)."""
51
+ if len(close) < n + 1:
52
+ return 0.0
53
+ tr = np.maximum.reduce([
54
+ high[-n:] - low[-n:],
55
+ np.abs(high[-n:] - close[-n - 1 : -1]),
56
+ np.abs(low[-n:] - close[-n - 1 : -1]),
57
+ ])
58
+ last = close[-1]
59
+ return float(np.mean(tr) / last) if last else 0.0
60
+
61
+
62
+ class SqueezeStrategy:
63
+ """Reference implementation of :class:`TradingStrategy`."""
64
+
65
+ def __init__(self, config: SqueezeConfig | None = None) -> None:
66
+ self.config = config or SqueezeConfig()
67
+ self._positions: dict[str, dict] = {}
68
+ self._highs: dict[str, np.ndarray] = {}
69
+ self._lows: dict[str, np.ndarray] = {}
70
+ self._atr_pct: dict[str, float] = {}
71
+
72
+ # ── position book ────────────────────────────────────────────────
73
+
74
+ @property
75
+ def active_positions(self) -> dict[str, dict]:
76
+ return {k: dict(v) for k, v in self._positions.items()}
77
+
78
+ def has_position(self, symbol: str) -> bool:
79
+ return symbol in self._positions
80
+
81
+ # ── data feed ────────────────────────────────────────────────────
82
+
83
+ def update_market_data(
84
+ self, symbol: str, high: np.ndarray, low: np.ndarray, close: np.ndarray
85
+ ) -> None:
86
+ self._highs[symbol] = np.asarray(high, dtype=float)
87
+ self._lows[symbol] = np.asarray(low, dtype=float)
88
+ self._atr_pct[symbol] = _atr_pct(
89
+ self._highs[symbol], self._lows[symbol],
90
+ np.asarray(close, dtype=float), self.config.atr_period,
91
+ )
92
+
93
+ # ── signal ───────────────────────────────────────────────────────
94
+
95
+ def _band_width(self, close: np.ndarray) -> np.ndarray:
96
+ """Rolling Bollinger band width (upper-lower) / mid, per bar."""
97
+ c = self.config
98
+ n = c.bb_period
99
+ widths = np.full(len(close), np.nan)
100
+ for i in range(n, len(close) + 1):
101
+ window = close[i - n : i]
102
+ mid = window.mean()
103
+ sd = window.std()
104
+ if mid:
105
+ widths[i - 1] = (2 * c.bb_std * sd) / mid
106
+ return widths
107
+
108
+ def on_bar(self, symbol: str, close: np.ndarray) -> int:
109
+ c = self.config
110
+ close = np.asarray(close, dtype=float)
111
+ if len(close) < max(c.bb_period, c.squeeze_lookback):
112
+ return 0
113
+
114
+ widths = self._band_width(close)
115
+ cur_width = widths[-1]
116
+ ref = widths[-c.squeeze_lookback :]
117
+ ref = ref[~np.isnan(ref)]
118
+ if np.isnan(cur_width) or len(ref) < c.squeeze_lookback // 2:
119
+ return 0
120
+
121
+ # In a squeeze? (band width in the bottom `squeeze_pct` of recent range)
122
+ threshold = np.quantile(ref, c.squeeze_pct)
123
+ if cur_width > threshold:
124
+ return 0
125
+
126
+ mid = _sma(close, c.bb_period)
127
+ sd = float(close[-c.bb_period :].std())
128
+ upper, lower = mid + c.bb_std * sd, mid - c.bb_std * sd
129
+ price = close[-1]
130
+ if price < lower:
131
+ return 1 # fade the downside extreme
132
+ if price > upper:
133
+ return -1 # fade the upside extreme
134
+ return 0
135
+
136
+ def get_last_atr_pct(self, symbol: str) -> float:
137
+ return self._atr_pct.get(symbol, 0.0)
138
+
139
+ # ── lifecycle ────────────────────────────────────────────────────
140
+
141
+ def open_position(
142
+ self, symbol: str, signal: int, entry_price: float, atr_pct: float
143
+ ) -> None:
144
+ self._positions[symbol] = {
145
+ "signal": signal,
146
+ "entry_price": entry_price,
147
+ "atr_pct": atr_pct,
148
+ "peak_gain": 0.0,
149
+ "trail_active": False,
150
+ }
151
+
152
+ def update_position(
153
+ self,
154
+ symbol: str,
155
+ price: float,
156
+ high: float | None = None,
157
+ low: float | None = None,
158
+ ) -> str | None:
159
+ pos = self._positions.get(symbol)
160
+ if pos is None:
161
+ return None
162
+ c = self.config
163
+ direction = pos["signal"]
164
+ entry = pos["entry_price"]
165
+ # Unrealised gain in the position's favour.
166
+ gain = direction * (price - entry) / entry
167
+
168
+ if gain <= -c.stop_loss:
169
+ return "stop_loss"
170
+ if gain >= c.take_profit:
171
+ return "take_profit"
172
+
173
+ # Trailing stop: arm at +trail_activate, then exit if we give back
174
+ # trail_distance from the peak.
175
+ pos["peak_gain"] = max(pos["peak_gain"], gain)
176
+ if pos["peak_gain"] >= c.trail_activate:
177
+ pos["trail_active"] = True
178
+ if pos["trail_active"] and gain <= pos["peak_gain"] - c.trail_distance:
179
+ return "trailing_stop"
180
+ return None
181
+
182
+ def close_position(self, symbol: str) -> dict | None:
183
+ return self._positions.pop(symbol, None)
@@ -0,0 +1,95 @@
1
+ """Strategy protocol — the formal contract every strategy must satisfy.
2
+
3
+ The engine (backtester and live scalper alike) only ever talks to a strategy
4
+ through this interface, never to a concrete class. That decoupling is what lets
5
+ the *same* engine run any strategy: a strategy is just an object that ingests
6
+ bars, emits signals, and tracks its own open positions and exit conditions.
7
+
8
+ Implemented via :pep:`544` structural subtyping — a strategy needs no base
9
+ class, only these methods. ``@runtime_checkable`` allows ``isinstance`` checks
10
+ in tests. The bundled :class:`~binance_quant_engine.strategy.demo_squeeze.SqueezeStrategy`
11
+ is a reference implementation on public, textbook logic.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Protocol, runtime_checkable
17
+
18
+ import numpy as np
19
+
20
+
21
+ @runtime_checkable
22
+ class TradingStrategy(Protocol):
23
+ """Interface for a stateful trading strategy.
24
+
25
+ Signal convention (returned by :meth:`on_bar`):
26
+ ``-1`` short · ``0`` flat/no-signal · ``1`` long.
27
+
28
+ Per-bar flow driven by the engine:
29
+ 1. :meth:`update_market_data` — feed the latest OHLCV window.
30
+ 2. :meth:`on_bar` — emit a signal for a symbol.
31
+ 3. :meth:`open_position` / :meth:`update_position` — lifecycle.
32
+ """
33
+
34
+ # ── position book ────────────────────────────────────────────────
35
+
36
+ @property
37
+ def active_positions(self) -> dict[str, dict]:
38
+ """Snapshot of open positions keyed by symbol."""
39
+ ...
40
+
41
+ def has_position(self, symbol: str) -> bool:
42
+ """Whether a position is currently open for *symbol*."""
43
+ ...
44
+
45
+ # ── data feed ────────────────────────────────────────────────────
46
+
47
+ def update_market_data(
48
+ self,
49
+ symbol: str,
50
+ high: np.ndarray,
51
+ low: np.ndarray,
52
+ close: np.ndarray,
53
+ ) -> None:
54
+ """Store the latest OHLC arrays for *symbol* before :meth:`on_bar`."""
55
+ ...
56
+
57
+ # ── signal generation ────────────────────────────────────────────
58
+
59
+ def on_bar(self, symbol: str, close: np.ndarray) -> int:
60
+ """Return a signal (``-1`` / ``0`` / ``1``) for the latest bar."""
61
+ ...
62
+
63
+ def get_last_atr_pct(self, symbol: str) -> float:
64
+ """Return the most recent ATR as a fraction of price (for sizing)."""
65
+ ...
66
+
67
+ # ── position lifecycle ───────────────────────────────────────────
68
+
69
+ def open_position(
70
+ self,
71
+ symbol: str,
72
+ signal: int,
73
+ entry_price: float,
74
+ atr_pct: float,
75
+ ) -> None:
76
+ """Record a newly opened position."""
77
+ ...
78
+
79
+ def update_position(
80
+ self,
81
+ symbol: str,
82
+ price: float,
83
+ high: float | None = None,
84
+ low: float | None = None,
85
+ ) -> str | None:
86
+ """Evaluate exit conditions for an open position.
87
+
88
+ Returns an exit-reason string (e.g. ``"stop_loss"``, ``"take_profit"``,
89
+ ``"trailing_stop"``) or ``None`` to keep the position open.
90
+ """
91
+ ...
92
+
93
+ def close_position(self, symbol: str) -> dict | None:
94
+ """Remove and return the position record, or ``None`` if absent."""
95
+ ...
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: binance-quant-engine
3
+ Version: 0.1.1
4
+ Summary: Strategy-agnostic Binance USDT-M futures backtest & execution engine — no-look-ahead, backtest/live parity, optional MCP server
5
+ Project-URL: Homepage, https://pypi.org/project/binance-quant-engine/
6
+ Project-URL: Repository, https://github.com/younghwan91/binance-quant-engine
7
+ Author-email: Younghwan Chae <chyohw97@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: algo-trading,backtest,binance,execution,futures,mcp,quant,trading
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: numpy>=1.26
13
+ Requires-Dist: pandas>=2.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: mcp>=1.0; extra == 'dev'
16
+ Requires-Dist: pytest>=8.0; extra == 'dev'
17
+ Requires-Dist: python-binance>=1.0.29; extra == 'dev'
18
+ Requires-Dist: ruff>=0.6; extra == 'dev'
19
+ Provides-Extra: live
20
+ Requires-Dist: python-binance>=1.0.29; extra == 'live'
21
+ Provides-Extra: mcp
22
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Binance Quant Engine ⚙️
26
+
27
+ [![CI](https://github.com/younghwan91/binance-quant-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/younghwan91/binance-quant-engine/actions/workflows/ci.yml)
28
+ [![PyPI](https://img.shields.io/pypi/v/binance-quant-engine.svg)](https://pypi.org/project/binance-quant-engine/)
29
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
31
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-younghwan--chae-0A66C2?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/younghwan-chae/)
32
+
33
+ **A strategy-agnostic Binance USDT-M futures backtest & execution engine.** Backtest and live trading run through the *same* strategy code, so a look-ahead bug can't exist in one and not the other — and an [MCP server](#mcp-server-let-an-agent-run-your-backtests) lets any MCP-compatible agent run backtests without writing a line of Python.
34
+
35
+ Extracted from the infra layer of a real, currently-running Binance USDT-M futures bot — the alpha (strategy, parameters, live P&L) stays private; what's public is the part every retail algo trader rebuilds badly at least once: a backtester that can't cheat, and an execution layer that survives Binance's actual API quirks. The bundled demo strategy is textbook logic (Bollinger squeeze); it exists to exercise the engine end to end, not to make you money.
36
+
37
+ > This repository supersedes `quantbox-engine`, which is no longer maintained. If you have that repo cloned, switch to this one.
38
+
39
+ ## Why not just write your own backtest loop?
40
+
41
+ Because the failure modes here are the ones that quietly wreck a real account, not the ones a unit test catches:
42
+
43
+ | Problem | What usually happens | What this engine does |
44
+ |---|---|---|
45
+ | Look-ahead bias | A signal computed over a full array accidentally sees future bars | The backtester physically hands the strategy only `close[: t + 1]` each step — there is no future in the array to peek at ([enforced by test](tests/test_backtest.py)) |
46
+ | Backtest/live divergence | Backtest logic gets "ported" to the live bot and drifts | One `TradingStrategy` object, same method calls, same order, in both paths |
47
+ | Binance tick-size rejection | A price a float-division away from the tick grid gets silently rejected or mispriced | [`round_to_tick`](src/binance_quant_engine/execution/utils.py) snaps every stop/limit/activation price to the symbol's grid before it leaves the process |
48
+ | Dead-man's switch | Bot crashes → open position has no stop-loss | SL/TP/trailing stops are placed **on Binance's Algo Order API**, so the exchange — not your process — enforces the exit |
49
+
50
+ ## Quick start
51
+
52
+ ```bash
53
+ pip install binance-quant-engine
54
+ # or: uv add binance-quant-engine
55
+
56
+ bqe-backtest --demo # backtest the bundled demo strategy on synthetic data
57
+ ```
58
+
59
+ ![Demo backtest run](docs/images/demo-backtest.png)
60
+
61
+ > ⚠️ The numbers above are from a **demo strategy on synthetic data**. They prove the engine runs; they say nothing about profitability.
62
+
63
+ ## Backtest your own data
64
+
65
+ ```python
66
+ from binance_quant_engine.backtest.vectorized import run_backtest
67
+ from binance_quant_engine.data.klines import load_csv
68
+ from binance_quant_engine.strategy.demo_squeeze import SqueezeStrategy
69
+
70
+ high, low, close = load_csv("BTCUSDT_1h.csv") # columns: high,low,close
71
+ result = run_backtest(SqueezeStrategy(), high, low, close, fee=0.0004, slippage=0.0002)
72
+ print(result.summary())
73
+ # {'n_trades': ..., 'total_return': ..., 'win_rate': ..., 'profit_factor': ..., 'max_drawdown': ...}
74
+ ```
75
+
76
+ Bring your own strategy by implementing [`TradingStrategy`](src/binance_quant_engine/strategy/protocol.py) — a `typing.Protocol`, no base class required. Wiring it into live execution: [docs/USAGE.md](docs/USAGE.md).
77
+
78
+ ## MCP server — let an agent run your backtests
79
+
80
+ ```bash
81
+ pip install "binance-quant-engine[mcp]"
82
+ binance-quant-engine-mcp # stdio MCP server
83
+ ```
84
+
85
+ Exposes `run_demo_backtest`, `backtest_csv`, and `describe_strategy_protocol` to any MCP client (Claude Code, Claude Desktop, etc.) — point an agent at a CSV of OHLC data and it can backtest a strategy idea in the same turn, no local Python environment required on the agent's side.
86
+
87
+ **This is deliberately backtest-only.** Nothing under `execution/` (order placement, cancellation, bracket management) is reachable through the MCP server — there is no tool call that can touch a live Binance order. If you want an agent that also *trades*, that's a decision you wire yourself, explicitly, outside this server.
88
+
89
+ ## Design
90
+
91
+ | Design choice | How |
92
+ |---|---|
93
+ | **No-look-ahead by construction** | The backtester hands the strategy `close[:t+1]` (bars completed as of now) every step — there's no future in the array to see. |
94
+ | **Backtest = live, same code** | The same `TradingStrategy` object is driven by the backtester and the live bot, in the same call order. No reimplementation gap. |
95
+ | **Pluggable strategy** | The engine only ever talks to a strategy through [`TradingStrategy`](src/binance_quant_engine/strategy/protocol.py) (PEP 544). No inheritance required. |
96
+ | **Costs are net, not gross** | Taker fee + slippage charged on both entry and exit legs. |
97
+ | **Server-side exits** | SL/TP/trailing stops live on Binance's Algo Order API — the exchange enforces them even if your process dies. ([brackets.py](src/binance_quant_engine/execution/brackets.py)) |
98
+ | **Tick-safe prices** | Every price sent to the exchange is snapped to the symbol's tick grid first ([utils.py](src/binance_quant_engine/execution/utils.py)) — a category of Binance rejection this engine doesn't have. |
99
+
100
+ ## Structure
101
+
102
+ **Decide** (strategy) / **measure** (backtest) / **execute** (live) are kept apart and meet only at `TradingStrategy`.
103
+
104
+ ```mermaid
105
+ flowchart LR
106
+ subgraph Data["binance_quant_engine/data/"]
107
+ CSV[("OHLCV CSV\nload_csv()")]
108
+ SYN[["synth_ohlcv()\nsynthetic data"]]
109
+ CACHE["cache.py\nmemory + gzip cache"]
110
+ end
111
+
112
+ subgraph Decide["binance_quant_engine/strategy/ — decide"]
113
+ PROTO{{"TradingStrategy\n(PEP 544 protocol)"}}
114
+ SQZ["demo_squeeze.py\nSqueezeStrategy"]
115
+ PROTO -.implements.-> SQZ
116
+ end
117
+
118
+ CSV --> BT
119
+ SYN --> BT
120
+ CACHE -.caches.-> CSV
121
+
122
+ subgraph Measure["binance_quant_engine/backtest/ — measure"]
123
+ BT["vectorized.run_backtest()\nhands close[:t+1] only\n(no look-ahead)"]
124
+ RES["BacktestResult\ntrades · equity curve · MDD"]
125
+ BT --> RES
126
+ end
127
+
128
+ BT <-->|"update_market_data / on_bar\nopen·update·close_position"| PROTO
129
+
130
+ subgraph Execute["binance_quant_engine/execution/ — execute (.[live])"]
131
+ HOST["host.py\nScalperProtocol (live bot host)"]
132
+ BRACKET["brackets.py\nBracketMixin — SL/TP/trailing"]
133
+ UTIL["utils.py\nround_to_tick · dead-status set"]
134
+ ALGO["algo_api.py\nAlgoApiClient"]
135
+ HOST --> BRACKET --> ALGO
136
+ BRACKET -.-> UTIL
137
+ end
138
+
139
+ subgraph MCP["binance_quant_engine/mcp/ — optional (.[mcp])"]
140
+ SRV["server.py\nrun_demo_backtest · backtest_csv"]
141
+ end
142
+ BT -.callable via.-> SRV
143
+
144
+ PROTO ==same interface\n(backtest = live)==> HOST
145
+ ALGO --> BINANCE[("Binance USDT-M\nFutures Algo Order API")]
146
+ ```
147
+
148
+ - How to plug in a strategy and wire up live execution → [docs/USAGE.md](docs/USAGE.md)
149
+ - Design rationale for no-look-ahead, backtest/live parity, and the cost model → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
150
+
151
+ ## License
152
+
153
+ MIT
154
+
155
+ ---
156
+
157
+ Bugs and questions → [Issues](https://github.com/younghwan91/binance-quant-engine/issues).
158
+
159
+ **Younghwan Chae** · [GitHub @younghwan91](https://github.com/younghwan91) · [LinkedIn](https://www.linkedin.com/in/younghwan-chae/) — other open-source quant projects (Korean equities, US equities, crypto) are on the [profile page](https://github.com/younghwan91).