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.
- binance_quant_engine/__init__.py +3 -0
- binance_quant_engine/backtest/__init__.py +1 -0
- binance_quant_engine/backtest/vectorized.py +177 -0
- binance_quant_engine/data/__init__.py +1 -0
- binance_quant_engine/data/cache.py +540 -0
- binance_quant_engine/data/klines.py +43 -0
- binance_quant_engine/execution/__init__.py +1 -0
- binance_quant_engine/execution/algo_api.py +84 -0
- binance_quant_engine/execution/brackets.py +973 -0
- binance_quant_engine/execution/host.py +65 -0
- binance_quant_engine/execution/utils.py +46 -0
- binance_quant_engine/mcp/__init__.py +1 -0
- binance_quant_engine/mcp/server.py +115 -0
- binance_quant_engine/strategy/__init__.py +1 -0
- binance_quant_engine/strategy/demo_squeeze.py +183 -0
- binance_quant_engine/strategy/protocol.py +95 -0
- binance_quant_engine-0.1.1.dist-info/METADATA +159 -0
- binance_quant_engine-0.1.1.dist-info/RECORD +21 -0
- binance_quant_engine-0.1.1.dist-info/WHEEL +4 -0
- binance_quant_engine-0.1.1.dist-info/entry_points.txt +3 -0
- binance_quant_engine-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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."""
|