binance-quant-engine 0.1.1__tar.gz
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-0.1.1/.env.example +5 -0
- binance_quant_engine-0.1.1/.github/workflows/ci.yml +49 -0
- binance_quant_engine-0.1.1/.github/workflows/commit-identity.yml +103 -0
- binance_quant_engine-0.1.1/.github/workflows/publish.yml +33 -0
- binance_quant_engine-0.1.1/.gitignore +9 -0
- binance_quant_engine-0.1.1/LICENSE +21 -0
- binance_quant_engine-0.1.1/PKG-INFO +159 -0
- binance_quant_engine-0.1.1/README.md +135 -0
- binance_quant_engine-0.1.1/docs/ARCHITECTURE.md +92 -0
- binance_quant_engine-0.1.1/docs/USAGE.md +102 -0
- binance_quant_engine-0.1.1/docs/images/demo-backtest.png +0 -0
- binance_quant_engine-0.1.1/pyproject.toml +45 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/__init__.py +3 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/backtest/__init__.py +1 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/backtest/vectorized.py +177 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/data/__init__.py +1 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/data/cache.py +540 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/data/klines.py +43 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/execution/__init__.py +1 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/execution/algo_api.py +84 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/execution/brackets.py +973 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/execution/host.py +65 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/execution/utils.py +46 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/mcp/__init__.py +1 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/mcp/server.py +115 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/strategy/__init__.py +1 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/strategy/demo_squeeze.py +183 -0
- binance_quant_engine-0.1.1/src/binance_quant_engine/strategy/protocol.py +95 -0
- binance_quant_engine-0.1.1/tests/test_backtest.py +71 -0
- binance_quant_engine-0.1.1/tests/test_demo_squeeze.py +51 -0
- binance_quant_engine-0.1.1/tests/test_execution_utils.py +33 -0
- binance_quant_engine-0.1.1/tests/test_mcp_server.py +37 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v7
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v7
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: pip install -e ".[dev,mcp]"
|
|
22
|
+
|
|
23
|
+
- name: Ruff
|
|
24
|
+
run: ruff check src tests
|
|
25
|
+
|
|
26
|
+
- name: Ruff format check
|
|
27
|
+
run: ruff format --check src tests
|
|
28
|
+
continue-on-error: true
|
|
29
|
+
|
|
30
|
+
test:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
strategy:
|
|
33
|
+
fail-fast: false
|
|
34
|
+
matrix:
|
|
35
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v7
|
|
39
|
+
|
|
40
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
41
|
+
uses: actions/setup-python@v7
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
|
|
45
|
+
- name: Install dependencies
|
|
46
|
+
run: pip install -e ".[dev,mcp]"
|
|
47
|
+
|
|
48
|
+
- name: Run tests
|
|
49
|
+
run: pytest tests/ -v
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
name: Commit Identity Guard
|
|
2
|
+
|
|
3
|
+
# 커밋 작성자/커미터 이메일이 허용 목록에 없으면 실패한다.
|
|
4
|
+
#
|
|
5
|
+
# 2026-08-29 사고: Claude Code 세션들이 세션 컨텍스트에 주어진 사용자 이메일
|
|
6
|
+
# (회사 주소)을 git 커밋 신원으로 오용해, `git -c user.email=<회사주소> commit`
|
|
7
|
+
# 형태로 커밋했다. 레포 로컬·전역 설정은 개인 이메일로 올바랐지만 명령줄이 그걸
|
|
8
|
+
# 덮었다. 그 결과 6개 레포에 회사 이메일 커밋이 쌓였고 3개는 PUBLIC 상태였다.
|
|
9
|
+
#
|
|
10
|
+
# 설정으로는 막을 수 없다(`-c`가 항상 이긴다). 그래서 CI에서 막는다.
|
|
11
|
+
#
|
|
12
|
+
# 허용 목록을 바꾸려면 ALLOWED_EMAILS를 수정하라.
|
|
13
|
+
# dependabot[bot] 은 봇 신원이다 — 회사 주소가 아니라서 허용한다.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
pull_request:
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
check-identity:
|
|
24
|
+
# 브랜치·태그 삭제 푸시엔 검사할 커밋도, checkout 할 ref 도 없다.
|
|
25
|
+
if: ${{ !github.event.deleted }}
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v7
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
|
|
32
|
+
- name: 커밋 신원 검사
|
|
33
|
+
env:
|
|
34
|
+
# 개인 이메일 + GitHub noreply만 허용.
|
|
35
|
+
# noreply@github.com 은 GitHub 머지 버튼이 만드는 커밋의 신원이다
|
|
36
|
+
# (Merge pull request #N ...). 회사 주소가 아니라 GitHub 자신이다.
|
|
37
|
+
# 에이전트 신원(noreply@anthropic.com 등)은 **허용하지 않는다** — 커밋은
|
|
38
|
+
# 사람 이름으로 남아야 한다. 에이전트 기여는 author/committer 가 아니라
|
|
39
|
+
# Co-Authored-By 트레일러로 적는다(그건 이 검사가 보지 않는다).
|
|
40
|
+
ALLOWED_EMAILS: |
|
|
41
|
+
chyohw97@gmail.com
|
|
42
|
+
48156556+younghwan91@users.noreply.github.com
|
|
43
|
+
noreply@github.com
|
|
44
|
+
49699333+dependabot[bot]@users.noreply.github.com
|
|
45
|
+
run: |
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
|
|
48
|
+
# PR이면 base..head, push면 이번에 밀린 커밋만 본다.
|
|
49
|
+
# 히스토리 전체를 검사하면 과거 사고 때문에 영원히 실패한다.
|
|
50
|
+
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
51
|
+
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
|
52
|
+
elif [ "${{ github.ref_type }}" = "tag" ]; then
|
|
53
|
+
# 태그 푸시는 새 커밋을 만들지 않는다. before 가 0 이라 아래 폴백을 타면
|
|
54
|
+
# 히스토리 50개를 다시 훑고, 과거 머지 커밋 때문에 영원히 실패한다.
|
|
55
|
+
# 기본 브랜치에 아직 없는 커밋만 본다 (보통 0개).
|
|
56
|
+
BASE="origin/${{ github.event.repository.default_branch }}"
|
|
57
|
+
if git rev-parse --verify -q "$BASE" >/dev/null; then
|
|
58
|
+
RANGE="$BASE..${{ github.sha }}"
|
|
59
|
+
else
|
|
60
|
+
RANGE="${{ github.sha }}~1..${{ github.sha }}"
|
|
61
|
+
fi
|
|
62
|
+
elif [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ] \
|
|
63
|
+
&& git cat-file -e "${{ github.event.before }}" 2>/dev/null; then
|
|
64
|
+
RANGE="${{ github.event.before }}..${{ github.sha }}"
|
|
65
|
+
else
|
|
66
|
+
# 새 브랜치/강제 푸시 등 before를 못 쓰는 경우: 최근 50개만
|
|
67
|
+
RANGE="$(git rev-list -n 50 ${{ github.sha }} | tail -1)^..${{ github.sha }}" || RANGE="${{ github.sha }}"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "검사 범위: $RANGE"
|
|
71
|
+
echo
|
|
72
|
+
|
|
73
|
+
# 허용 목록을 정규화 (빈 줄·공백 제거)
|
|
74
|
+
ALLOWED=$(echo "$ALLOWED_EMAILS" | tr -d ' ' | grep -v '^$' | sort -u)
|
|
75
|
+
echo "허용된 이메일:"
|
|
76
|
+
echo "$ALLOWED" | sed 's/^/ /'
|
|
77
|
+
echo
|
|
78
|
+
|
|
79
|
+
BAD=0
|
|
80
|
+
while IFS='|' read -r sha author committer subject; do
|
|
81
|
+
[ -z "$sha" ] && continue
|
|
82
|
+
for email in "$author" "$committer"; do
|
|
83
|
+
if ! echo "$ALLOWED" | grep -Fxq "$email"; then
|
|
84
|
+
echo "::error::${sha:0:8} 허용되지 않은 이메일: $email ($subject)"
|
|
85
|
+
BAD=1
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
done < <(git log --format='%H|%ae|%ce|%s' "$RANGE" 2>/dev/null || true)
|
|
89
|
+
|
|
90
|
+
if [ "$BAD" -eq 1 ]; then
|
|
91
|
+
echo
|
|
92
|
+
echo "커밋 신원이 허용 목록에 없다. 회사/조직 이메일이 섞였을 가능성이 높다."
|
|
93
|
+
echo
|
|
94
|
+
echo "고치는 법:"
|
|
95
|
+
echo " git config user.email chyohw97@gmail.com"
|
|
96
|
+
echo " git rebase -i --exec 'git commit --amend --reset-author --no-edit' <base>"
|
|
97
|
+
echo " git push --force-with-lease"
|
|
98
|
+
echo
|
|
99
|
+
echo "주의: 'git -c user.email=...' 로 신원을 덮어쓰지 말 것."
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
echo "✅ 커밋 신원 이상 없음"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: release
|
|
12
|
+
permissions:
|
|
13
|
+
# id-token 만 적으면 나머지 스코프가 전부 none 이 된다.
|
|
14
|
+
# 그러면 GITHUB_TOKEN 이 레포를 못 읽어 checkout 이
|
|
15
|
+
# "remote: Repository not found" 로 죽는다 (레포가 비공개일 때).
|
|
16
|
+
contents: read
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v7
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-python@v7
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
|
|
26
|
+
- name: Install build
|
|
27
|
+
run: pip install build
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: python -m build
|
|
31
|
+
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Younghwan Chae
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -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
|
+
[](https://github.com/younghwan91/binance-quant-engine/actions/workflows/ci.yml)
|
|
28
|
+
[](https://pypi.org/project/binance-quant-engine/)
|
|
29
|
+
[](https://www.python.org/downloads/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
[](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
|
+

|
|
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).
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Binance Quant Engine ⚙️
|
|
2
|
+
|
|
3
|
+
[](https://github.com/younghwan91/binance-quant-engine/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/binance-quant-engine/)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://www.linkedin.com/in/younghwan-chae/)
|
|
8
|
+
|
|
9
|
+
**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.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
> This repository supersedes `quantbox-engine`, which is no longer maintained. If you have that repo cloned, switch to this one.
|
|
14
|
+
|
|
15
|
+
## Why not just write your own backtest loop?
|
|
16
|
+
|
|
17
|
+
Because the failure modes here are the ones that quietly wreck a real account, not the ones a unit test catches:
|
|
18
|
+
|
|
19
|
+
| Problem | What usually happens | What this engine does |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| 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)) |
|
|
22
|
+
| Backtest/live divergence | Backtest logic gets "ported" to the live bot and drifts | One `TradingStrategy` object, same method calls, same order, in both paths |
|
|
23
|
+
| 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 |
|
|
24
|
+
| 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 |
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install binance-quant-engine
|
|
30
|
+
# or: uv add binance-quant-engine
|
|
31
|
+
|
|
32
|
+
bqe-backtest --demo # backtest the bundled demo strategy on synthetic data
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
> ⚠️ The numbers above are from a **demo strategy on synthetic data**. They prove the engine runs; they say nothing about profitability.
|
|
38
|
+
|
|
39
|
+
## Backtest your own data
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from binance_quant_engine.backtest.vectorized import run_backtest
|
|
43
|
+
from binance_quant_engine.data.klines import load_csv
|
|
44
|
+
from binance_quant_engine.strategy.demo_squeeze import SqueezeStrategy
|
|
45
|
+
|
|
46
|
+
high, low, close = load_csv("BTCUSDT_1h.csv") # columns: high,low,close
|
|
47
|
+
result = run_backtest(SqueezeStrategy(), high, low, close, fee=0.0004, slippage=0.0002)
|
|
48
|
+
print(result.summary())
|
|
49
|
+
# {'n_trades': ..., 'total_return': ..., 'win_rate': ..., 'profit_factor': ..., 'max_drawdown': ...}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
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).
|
|
53
|
+
|
|
54
|
+
## MCP server — let an agent run your backtests
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install "binance-quant-engine[mcp]"
|
|
58
|
+
binance-quant-engine-mcp # stdio MCP server
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
63
|
+
**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.
|
|
64
|
+
|
|
65
|
+
## Design
|
|
66
|
+
|
|
67
|
+
| Design choice | How |
|
|
68
|
+
|---|---|
|
|
69
|
+
| **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. |
|
|
70
|
+
| **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. |
|
|
71
|
+
| **Pluggable strategy** | The engine only ever talks to a strategy through [`TradingStrategy`](src/binance_quant_engine/strategy/protocol.py) (PEP 544). No inheritance required. |
|
|
72
|
+
| **Costs are net, not gross** | Taker fee + slippage charged on both entry and exit legs. |
|
|
73
|
+
| **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)) |
|
|
74
|
+
| **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. |
|
|
75
|
+
|
|
76
|
+
## Structure
|
|
77
|
+
|
|
78
|
+
**Decide** (strategy) / **measure** (backtest) / **execute** (live) are kept apart and meet only at `TradingStrategy`.
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
flowchart LR
|
|
82
|
+
subgraph Data["binance_quant_engine/data/"]
|
|
83
|
+
CSV[("OHLCV CSV\nload_csv()")]
|
|
84
|
+
SYN[["synth_ohlcv()\nsynthetic data"]]
|
|
85
|
+
CACHE["cache.py\nmemory + gzip cache"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
subgraph Decide["binance_quant_engine/strategy/ — decide"]
|
|
89
|
+
PROTO{{"TradingStrategy\n(PEP 544 protocol)"}}
|
|
90
|
+
SQZ["demo_squeeze.py\nSqueezeStrategy"]
|
|
91
|
+
PROTO -.implements.-> SQZ
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
CSV --> BT
|
|
95
|
+
SYN --> BT
|
|
96
|
+
CACHE -.caches.-> CSV
|
|
97
|
+
|
|
98
|
+
subgraph Measure["binance_quant_engine/backtest/ — measure"]
|
|
99
|
+
BT["vectorized.run_backtest()\nhands close[:t+1] only\n(no look-ahead)"]
|
|
100
|
+
RES["BacktestResult\ntrades · equity curve · MDD"]
|
|
101
|
+
BT --> RES
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
BT <-->|"update_market_data / on_bar\nopen·update·close_position"| PROTO
|
|
105
|
+
|
|
106
|
+
subgraph Execute["binance_quant_engine/execution/ — execute (.[live])"]
|
|
107
|
+
HOST["host.py\nScalperProtocol (live bot host)"]
|
|
108
|
+
BRACKET["brackets.py\nBracketMixin — SL/TP/trailing"]
|
|
109
|
+
UTIL["utils.py\nround_to_tick · dead-status set"]
|
|
110
|
+
ALGO["algo_api.py\nAlgoApiClient"]
|
|
111
|
+
HOST --> BRACKET --> ALGO
|
|
112
|
+
BRACKET -.-> UTIL
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
subgraph MCP["binance_quant_engine/mcp/ — optional (.[mcp])"]
|
|
116
|
+
SRV["server.py\nrun_demo_backtest · backtest_csv"]
|
|
117
|
+
end
|
|
118
|
+
BT -.callable via.-> SRV
|
|
119
|
+
|
|
120
|
+
PROTO ==same interface\n(backtest = live)==> HOST
|
|
121
|
+
ALGO --> BINANCE[("Binance USDT-M\nFutures Algo Order API")]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- How to plug in a strategy and wire up live execution → [docs/USAGE.md](docs/USAGE.md)
|
|
125
|
+
- Design rationale for no-look-ahead, backtest/live parity, and the cost model → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
Bugs and questions → [Issues](https://github.com/younghwan91/binance-quant-engine/issues).
|
|
134
|
+
|
|
135
|
+
**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).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Binance Quant Engine separates three concerns that are usually tangled in trading
|
|
4
|
+
code: **deciding** (strategy), **measuring** (backtest), and **executing**
|
|
5
|
+
(live). The engine owns measuring and executing; a strategy only decides. They
|
|
6
|
+
meet at one narrow interface, [`TradingStrategy`](../src/binance_quant_engine/strategy/protocol.py).
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
┌───────────────────────────────┐
|
|
10
|
+
OHLCV → │ Backtester / Live Scalper │ ← same driver, same calls
|
|
11
|
+
└───────────────┬───────────────┘
|
|
12
|
+
│ TradingStrategy protocol
|
|
13
|
+
▼
|
|
14
|
+
┌───────────────────────────────┐
|
|
15
|
+
│ Strategy (e.g. Squeeze) │ decides: -1 / 0 / +1, exits
|
|
16
|
+
└───────────────────────────────┘
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 1. No-look-ahead by construction
|
|
20
|
+
|
|
21
|
+
The cheapest way to fake a great backtest is to let a signal see the future.
|
|
22
|
+
We remove the possibility instead of policing it: the backtester only ever
|
|
23
|
+
hands a strategy `close[: t + 1]`, the prefix of *completed* bars up to the
|
|
24
|
+
current one. A strategy literally cannot index into the future because the
|
|
25
|
+
future isn't in the array it receives.
|
|
26
|
+
|
|
27
|
+
This invariant is asserted in
|
|
28
|
+
[`test_no_look_ahead_strategy_only_sees_past`](../tests/test_backtest.py): a spy
|
|
29
|
+
strategy records every window it is shown and verifies each is an exact past
|
|
30
|
+
prefix that grows by one bar per step.
|
|
31
|
+
|
|
32
|
+
## 2. Backtest / live parity
|
|
33
|
+
|
|
34
|
+
A strategy is a *stateful object* implementing `TradingStrategy`, not a vector
|
|
35
|
+
of precomputed signals. The exact same object is stepped by the backtester and
|
|
36
|
+
by the live scalper, calling the same methods in the same order:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
update_market_data(...) # feed the latest completed bars
|
|
40
|
+
on_bar(...) # -> signal
|
|
41
|
+
open_position(...) # on entry
|
|
42
|
+
update_position(...) # -> exit reason or None, each subsequent bar
|
|
43
|
+
close_position(...)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Because there is one implementation of the decision logic, a backtested edge and
|
|
47
|
+
a live edge cannot silently diverge from a reimplementation gap. The only
|
|
48
|
+
differences live in the *execution* layer (fills, latency, fees), which the
|
|
49
|
+
backtester models explicitly.
|
|
50
|
+
|
|
51
|
+
## 3. Execution layer (live)
|
|
52
|
+
|
|
53
|
+
The live layer (in [`src/binance_quant_engine/execution/`](../src/binance_quant_engine/execution/)) is included as a
|
|
54
|
+
reference for how the decision logic reaches the exchange safely. Highlights:
|
|
55
|
+
|
|
56
|
+
- **Server-side brackets** ([`brackets.py`](../src/binance_quant_engine/execution/brackets.py)):
|
|
57
|
+
stop-loss / take-profit / trailing stops are placed on Binance's Algo Order
|
|
58
|
+
engine, so risk protection survives a bot crash, restart, or network outage —
|
|
59
|
+
the exchange enforces the exit even if the process is dead.
|
|
60
|
+
- **Ratcheting trailing stops**: as price advances, the trailing stop is moved
|
|
61
|
+
(ratcheted) server-side, never loosened, with a minimum step to avoid
|
|
62
|
+
rate-limit churn.
|
|
63
|
+
- **Host interface** ([`host.py`](../src/binance_quant_engine/execution/host.py)): execution is
|
|
64
|
+
composed from mixins typed against a `ScalperProtocol`, so each mixin
|
|
65
|
+
type-checks in isolation without importing a concrete, strategy-specific host.
|
|
66
|
+
|
|
67
|
+
> Trailing geometry here is driven by a generic `TrailConfig`; the proprietary
|
|
68
|
+
> per-strategy tuning from the private system is intentionally not included.
|
|
69
|
+
|
|
70
|
+
## 4. Cost model
|
|
71
|
+
|
|
72
|
+
`Trade.net_return` charges `fee + slippage` on **both** the entry and exit legs,
|
|
73
|
+
so `BacktestResult.profit_factor`, `win_rate`, and `total_return` are all net of
|
|
74
|
+
trading costs. Gross return is available separately for attribution.
|
|
75
|
+
|
|
76
|
+
## 5. MCP server (optional, `.[mcp]`)
|
|
77
|
+
|
|
78
|
+
[`src/binance_quant_engine/mcp/server.py`](../src/binance_quant_engine/mcp/server.py) exposes the
|
|
79
|
+
backtest engine — not the execution layer — over the Model Context Protocol,
|
|
80
|
+
so any MCP-compatible agent can run `run_demo_backtest` / `backtest_csv`
|
|
81
|
+
without writing Python. This boundary is deliberate: nothing under
|
|
82
|
+
`execution/` is reachable from the MCP process, so there is no tool call that
|
|
83
|
+
can place, modify, or cancel a live order. Wire `execution/` into your own bot
|
|
84
|
+
directly if you want live trading; don't expect (or ask for) an MCP tool that
|
|
85
|
+
does it.
|
|
86
|
+
|
|
87
|
+
## What is intentionally omitted
|
|
88
|
+
|
|
89
|
+
This is the engine, not the alpha. The proprietary strategies, their parameters,
|
|
90
|
+
research notebooks, and live performance figures from the private system are not
|
|
91
|
+
part of this repository. The bundled `SqueezeStrategy` is public textbook logic
|
|
92
|
+
whose only job is to exercise the engine end to end.
|