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,3 @@
1
+ """Binance Quant Engine — a strategy-agnostic crypto-futures backtest & execution engine."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1 @@
1
+ """Backtest engine."""
@@ -0,0 +1,177 @@
1
+ """Event-stepped backtest engine with a strict no-look-ahead guarantee.
2
+
3
+ The single most common way a backtest lies is by letting a signal peek at data
4
+ that would not yet exist in live trading. This engine makes that structurally
5
+ impossible: at bar ``t`` the strategy is handed only ``close[: t + 1]`` — the
6
+ slice of *completed* bars up to and including ``t`` — and the resulting signal
7
+ can only act from bar ``t`` onward. The same strategy object that runs here is
8
+ the one the live scalper drives, so backtest and live share one code path.
9
+
10
+ Costs (taker fee + slippage) are charged on entry and exit so headline numbers
11
+ are net, not gross.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ from dataclasses import dataclass, field
18
+
19
+ import numpy as np
20
+
21
+ from binance_quant_engine.strategy.protocol import TradingStrategy
22
+
23
+
24
+ @dataclass
25
+ class Trade:
26
+ entry_idx: int
27
+ exit_idx: int
28
+ signal: int
29
+ entry_price: float
30
+ exit_price: float
31
+ reason: str
32
+ cost: float = 0.0006 # per-leg cost (fee + slippage) as a fraction
33
+
34
+ @property
35
+ def gross_return(self) -> float:
36
+ return self.signal * (self.exit_price - self.entry_price) / self.entry_price
37
+
38
+ @property
39
+ def net_return(self) -> float:
40
+ """Return after charging ``cost`` on both entry and exit legs."""
41
+ return self.gross_return - 2 * self.cost
42
+
43
+
44
+ @dataclass
45
+ class BacktestResult:
46
+ trades: list[Trade] = field(default_factory=list)
47
+ equity_curve: np.ndarray = field(default_factory=lambda: np.array([1.0]))
48
+
49
+ @property
50
+ def n_trades(self) -> int:
51
+ return len(self.trades)
52
+
53
+ @property
54
+ def total_return(self) -> float:
55
+ return float(self.equity_curve[-1] - 1.0)
56
+
57
+ @property
58
+ def win_rate(self) -> float:
59
+ if not self.trades:
60
+ return 0.0
61
+ wins = sum(1 for t in self.trades if t.net_return > 0)
62
+ return wins / len(self.trades)
63
+
64
+ @property
65
+ def profit_factor(self) -> float:
66
+ gains = sum(t.net_return for t in self.trades if t.net_return > 0)
67
+ losses = -sum(t.net_return for t in self.trades if t.net_return < 0)
68
+ if losses == 0:
69
+ return float("inf") if gains > 0 else 0.0
70
+ return gains / losses
71
+
72
+ @property
73
+ def max_drawdown(self) -> float:
74
+ peak = np.maximum.accumulate(self.equity_curve)
75
+ return float(np.min(self.equity_curve / peak - 1.0))
76
+
77
+ def summary(self) -> dict[str, float]:
78
+ return {
79
+ "n_trades": self.n_trades,
80
+ "total_return": self.total_return,
81
+ "win_rate": self.win_rate,
82
+ "profit_factor": self.profit_factor,
83
+ "max_drawdown": self.max_drawdown,
84
+ }
85
+
86
+
87
+ def run_backtest(
88
+ strategy: TradingStrategy,
89
+ high: np.ndarray,
90
+ low: np.ndarray,
91
+ close: np.ndarray,
92
+ *,
93
+ symbol: str = "DEMO",
94
+ fee: float = 0.0004, # 4 bps taker
95
+ slippage: float = 0.0002, # 2 bps
96
+ warmup: int = 100,
97
+ ) -> BacktestResult:
98
+ """Step a strategy bar-by-bar over one OHLC series; return performance.
99
+
100
+ At each bar the strategy sees only completed history (``[: t + 1]``). A
101
+ flat strategy may open on the bar that produced the signal; an open position
102
+ is checked for exits each subsequent bar. Returns are net of ``fee`` and
103
+ ``slippage`` charged on both legs.
104
+ """
105
+ high = np.asarray(high, float)
106
+ low = np.asarray(low, float)
107
+ close = np.asarray(close, float)
108
+ n = len(close)
109
+
110
+ equity = 1.0
111
+ curve = [equity]
112
+ trades: list[Trade] = []
113
+ open_trade: dict | None = None
114
+ cost = fee + slippage # per-leg cost as a fraction
115
+
116
+ for t in range(warmup, n):
117
+ # ── no-look-ahead: strategy only ever sees bars up to t ──
118
+ h, lo, c = high[: t + 1], low[: t + 1], close[: t + 1]
119
+ strategy.update_market_data(symbol, h, lo, c)
120
+ price = close[t]
121
+
122
+ if open_trade is None:
123
+ signal = strategy.on_bar(symbol, c)
124
+ if signal != 0:
125
+ atr_pct = strategy.get_last_atr_pct(symbol)
126
+ strategy.open_position(symbol, signal, price, atr_pct)
127
+ open_trade = {"entry_idx": t, "entry_price": price, "signal": signal}
128
+ else:
129
+ reason = strategy.update_position(symbol, price, high[t], low[t])
130
+ if reason is not None:
131
+ strategy.close_position(symbol)
132
+ tr = Trade(
133
+ entry_idx=open_trade["entry_idx"],
134
+ exit_idx=t,
135
+ signal=open_trade["signal"],
136
+ entry_price=open_trade["entry_price"],
137
+ exit_price=price,
138
+ reason=reason,
139
+ cost=cost,
140
+ )
141
+ trades.append(tr)
142
+ equity *= 1.0 + tr.net_return
143
+ open_trade = None
144
+ curve.append(equity)
145
+
146
+ return BacktestResult(trades=trades, equity_curve=np.array(curve))
147
+
148
+
149
+ def _demo() -> int:
150
+ from binance_quant_engine.data.klines import synth_ohlcv
151
+
152
+ high, low, close = synth_ohlcv(n=2000, seed=7)
153
+ from binance_quant_engine.strategy.demo_squeeze import SqueezeStrategy
154
+
155
+ result = run_backtest(SqueezeStrategy(), high, low, close)
156
+ s = result.summary()
157
+ print("Binance Quant Engine — demo backtest (Bollinger squeeze, synthetic data)")
158
+ print(f" trades : {s['n_trades']}")
159
+ print(f" total return : {s['total_return']:+.2%}")
160
+ print(f" win rate : {s['win_rate']:.1%}")
161
+ print(f" profit factor : {s['profit_factor']:.2f}")
162
+ print(f" max drawdown : {s['max_drawdown']:.2%}")
163
+ return 0
164
+
165
+
166
+ def main() -> int:
167
+ parser = argparse.ArgumentParser(description="Binance Quant Engine backtest")
168
+ parser.add_argument("--demo", action="store_true", help="run the bundled demo")
169
+ args = parser.parse_args()
170
+ if args.demo:
171
+ return _demo()
172
+ parser.print_help()
173
+ return 0
174
+
175
+
176
+ if __name__ == "__main__":
177
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Data loading and caching."""