clawquant 0.1.0__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.
- clawquant/__init__.py +1 -0
- clawquant/clawquant_cli.py +303 -0
- clawquant/cli/__init__.py +0 -0
- clawquant/cli/backtest_cli.py +233 -0
- clawquant/cli/data_cli.py +101 -0
- clawquant/cli/deploy_cli.py +154 -0
- clawquant/cli/radar_cli.py +98 -0
- clawquant/cli/report_cli.py +80 -0
- clawquant/cli/strategy_cli.py +215 -0
- clawquant/core/__init__.py +0 -0
- clawquant/core/backtest/__init__.py +0 -0
- clawquant/core/backtest/batch.py +119 -0
- clawquant/core/backtest/config.py +36 -0
- clawquant/core/backtest/engine.py +346 -0
- clawquant/core/backtest/events.py +58 -0
- clawquant/core/backtest/execution.py +67 -0
- clawquant/core/backtest/portfolio.py +170 -0
- clawquant/core/backtest/result.py +69 -0
- clawquant/core/backtest/risk.py +85 -0
- clawquant/core/backtest/sweep.py +130 -0
- clawquant/core/backtest/walkforward.py +169 -0
- clawquant/core/data/__init__.py +0 -0
- clawquant/core/data/alignment.py +91 -0
- clawquant/core/data/cache.py +120 -0
- clawquant/core/data/fetcher.py +150 -0
- clawquant/core/data/inspector.py +119 -0
- clawquant/core/data/models.py +59 -0
- clawquant/core/deploy/__init__.py +0 -0
- clawquant/core/deploy/manager.py +72 -0
- clawquant/core/deploy/runner.py +196 -0
- clawquant/core/evaluate/__init__.py +0 -0
- clawquant/core/evaluate/metrics.py +154 -0
- clawquant/core/evaluate/scorer.py +137 -0
- clawquant/core/radar/__init__.py +0 -0
- clawquant/core/radar/explainer.py +89 -0
- clawquant/core/radar/scanner.py +150 -0
- clawquant/core/report/__init__.py +0 -0
- clawquant/core/report/charts.py +133 -0
- clawquant/core/report/generator.py +155 -0
- clawquant/core/report/json_report.py +40 -0
- clawquant/core/report/markdown_report.py +127 -0
- clawquant/core/runtime/__init__.py +0 -0
- clawquant/core/runtime/base_strategy.py +116 -0
- clawquant/core/runtime/loader.py +370 -0
- clawquant/core/runtime/models.py +45 -0
- clawquant/core/runtime/sandbox.py +88 -0
- clawquant/core/utils/__init__.py +0 -0
- clawquant/core/utils/logging.py +64 -0
- clawquant/core/utils/output.py +104 -0
- clawquant/core/utils/run_id.py +69 -0
- clawquant/core/utils/state.py +12 -0
- clawquant/integrations/__init__.py +0 -0
- clawquant/integrations/binance_skill_wrapper/__init__.py +0 -0
- clawquant/integrations/binance_skill_wrapper/wrapper.py +75 -0
- clawquant/integrations/ccxt_fallback/__init__.py +0 -0
- clawquant/integrations/ccxt_fallback/client.py +154 -0
- clawquant/strategies_builtin/__init__.py +11 -0
- clawquant/strategies_builtin/dca.py +149 -0
- clawquant/strategies_builtin/grid.py +249 -0
- clawquant/strategies_builtin/ma_crossover.py +202 -0
- clawquant-0.1.0.dist-info/METADATA +170 -0
- clawquant-0.1.0.dist-info/RECORD +66 -0
- clawquant-0.1.0.dist-info/WHEEL +5 -0
- clawquant-0.1.0.dist-info/entry_points.txt +2 -0
- clawquant-0.1.0.dist-info/licenses/LICENSE +21 -0
- clawquant-0.1.0.dist-info/top_level.txt +1 -0
clawquant/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""ClawQuant Trader - CLI entry point.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m clawquant.clawquant_cli --help
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from clawquant import __version__
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Main app
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
name="clawquant",
|
|
18
|
+
help="ClawQuant Trader - Quantitative Research Infrastructure",
|
|
19
|
+
add_completion=False,
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Global state (stored in shared module to survive python -m re-import)
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
from clawquant.core.utils.state import get_json_mode as _get_json_mode, set_json_mode
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _version_callback(value: bool) -> None:
|
|
30
|
+
if value:
|
|
31
|
+
typer.echo(f"ClawQuant Trader v{__version__}")
|
|
32
|
+
raise typer.Exit()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.callback()
|
|
36
|
+
def main(
|
|
37
|
+
version: Optional[bool] = typer.Option(
|
|
38
|
+
None, "--version", "-V", help="Show version and exit.",
|
|
39
|
+
callback=_version_callback, is_eager=True,
|
|
40
|
+
),
|
|
41
|
+
json_output: bool = typer.Option(
|
|
42
|
+
False, "--json", help="Output results as JSON instead of Rich tables.",
|
|
43
|
+
),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""ClawQuant Trader - Quantitative Research Infrastructure."""
|
|
46
|
+
set_json_mode(json_output)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Sub-app: data
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
data_app = typer.Typer(help="Data management commands", no_args_is_help=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@data_app.command("pull")
|
|
56
|
+
def data_pull(
|
|
57
|
+
symbols: str = typer.Argument(..., help="Comma-separated symbols, e.g. BTC/USDT,ETH/USDT"),
|
|
58
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
59
|
+
days: int = typer.Option(10, "--days", "-d", help="Number of days to fetch"),
|
|
60
|
+
exchange: str = typer.Option("binance", "--exchange", "-e", help="Exchange name"),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Pull OHLCV data for one or more symbols."""
|
|
63
|
+
from clawquant.cli.data_cli import pull
|
|
64
|
+
pull(symbols=symbols, interval=interval, days=days, exchange=exchange, json_mode=_get_json_mode())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@data_app.command("inspect")
|
|
68
|
+
def data_inspect(
|
|
69
|
+
symbol: str = typer.Argument(..., help="Symbol to check, e.g. BTC/USDT"),
|
|
70
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Run data quality checks on a cached dataset."""
|
|
73
|
+
from clawquant.cli.data_cli import inspect
|
|
74
|
+
inspect(symbol=symbol, interval=interval, json_mode=_get_json_mode())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@data_app.command("cache-status")
|
|
78
|
+
def data_cache_status() -> None:
|
|
79
|
+
"""Show all cached data files."""
|
|
80
|
+
from clawquant.cli.data_cli import cache_status
|
|
81
|
+
cache_status(json_mode=_get_json_mode())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
app.add_typer(data_app, name="data")
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Sub-app: strategy (delegated)
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
from clawquant.cli.strategy_cli import strategy_app # noqa: E402
|
|
90
|
+
|
|
91
|
+
app.add_typer(strategy_app, name="strategy")
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Sub-app: backtest
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
backtest_app = typer.Typer(help="Backtesting commands", no_args_is_help=True)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@backtest_app.command("run")
|
|
100
|
+
def backtest_run(
|
|
101
|
+
strategy: str = typer.Argument(..., help="Strategy name"),
|
|
102
|
+
symbol: str = typer.Option("BTC/USDT", "--symbol", "-s", help="Trading pair"),
|
|
103
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
104
|
+
days: int = typer.Option(30, "--days", "-d", help="Backtest window in days"),
|
|
105
|
+
capital: float = typer.Option(10000.0, "--capital", "-c", help="Initial capital in USDT"),
|
|
106
|
+
fee_bps: int = typer.Option(10, "--fee-bps", help="Fee in basis points"),
|
|
107
|
+
slippage_bps: int = typer.Option(5, "--slippage-bps", help="Slippage in basis points"),
|
|
108
|
+
params: Optional[str] = typer.Option(None, "--params", "-p", help='Strategy params JSON, e.g. \'{"fast_period": 10}\''),
|
|
109
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Check readiness without running"),
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Run a single backtest."""
|
|
112
|
+
from clawquant.cli.backtest_cli import run
|
|
113
|
+
run(strategy=strategy, symbol=symbol, interval=interval, days=days,
|
|
114
|
+
capital=capital, fee_bps=fee_bps, slippage_bps=slippage_bps,
|
|
115
|
+
params=params, dry_run=dry_run, json_mode=_get_json_mode())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@backtest_app.command("batch")
|
|
119
|
+
def backtest_batch(
|
|
120
|
+
strategies: str = typer.Argument(..., help="Comma-separated strategy names"),
|
|
121
|
+
symbols: str = typer.Option("BTC/USDT", "--symbols", "-s", help="Comma-separated symbols"),
|
|
122
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
123
|
+
days: int = typer.Option(30, "--days", "-d", help="Backtest window in days"),
|
|
124
|
+
capital: float = typer.Option(10000.0, "--capital", "-c", help="Initial capital"),
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Run batch backtests across strategies and symbols."""
|
|
127
|
+
from clawquant.cli.backtest_cli import batch
|
|
128
|
+
batch(strategies=strategies, symbols=symbols, interval=interval, days=days,
|
|
129
|
+
capital=capital, json_mode=_get_json_mode())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@backtest_app.command("sweep")
|
|
133
|
+
def backtest_sweep(
|
|
134
|
+
strategy: str = typer.Argument(..., help="Strategy name"),
|
|
135
|
+
symbol: str = typer.Option("BTC/USDT", "--symbol", "-s", help="Trading pair"),
|
|
136
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
137
|
+
days: int = typer.Option(30, "--days", "-d", help="Backtest window"),
|
|
138
|
+
param_grid: Optional[str] = typer.Option(None, "--grid", "-g", help='Param grid JSON, e.g. \'{"fast_period": [5,10,20]}\''),
|
|
139
|
+
mode: str = typer.Option("grid", "--mode", "-m", help="Sweep mode: grid or random"),
|
|
140
|
+
n_random: int = typer.Option(20, "--n-random", help="Number of random samples"),
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Run parameter sweep over a strategy."""
|
|
143
|
+
from clawquant.cli.backtest_cli import sweep
|
|
144
|
+
sweep(strategy=strategy, symbol=symbol, interval=interval, days=days,
|
|
145
|
+
param_grid=param_grid, mode=mode, n_random=n_random, json_mode=_get_json_mode())
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@backtest_app.command("walkforward")
|
|
149
|
+
def backtest_walkforward(
|
|
150
|
+
strategy: str = typer.Argument(..., help="Strategy name"),
|
|
151
|
+
symbol: str = typer.Option("BTC/USDT", "--symbol", "-s", help="Trading pair"),
|
|
152
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
153
|
+
days: int = typer.Option(90, "--days", "-d", help="Total data window"),
|
|
154
|
+
train_pct: float = typer.Option(0.7, "--train-pct", help="Training fraction"),
|
|
155
|
+
n_splits: int = typer.Option(3, "--splits", "-n", help="Number of rolling splits"),
|
|
156
|
+
param_grid: Optional[str] = typer.Option(None, "--grid", "-g", help="Param grid JSON"),
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Run walk-forward validation."""
|
|
159
|
+
from clawquant.cli.backtest_cli import walkforward
|
|
160
|
+
walkforward(strategy=strategy, symbol=symbol, interval=interval, days=days,
|
|
161
|
+
train_pct=train_pct, n_splits=n_splits, param_grid=param_grid,
|
|
162
|
+
json_mode=_get_json_mode())
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
app.add_typer(backtest_app, name="backtest")
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Sub-app: radar
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
radar_app = typer.Typer(help="Opportunity scanning commands", no_args_is_help=True)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@radar_app.command("scan")
|
|
174
|
+
def radar_scan(
|
|
175
|
+
symbols: str = typer.Option("BTC/USDT,ETH/USDT", "--symbols", "-s", help="Comma-separated symbols"),
|
|
176
|
+
strategies: str = typer.Option("ma_crossover,dca", "--strategies", help="Comma-separated strategies"),
|
|
177
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
178
|
+
days: int = typer.Option(10, "--days", "-d", help="Data window"),
|
|
179
|
+
exchange: str = typer.Option("binance", "--exchange", "-e", help="Exchange"),
|
|
180
|
+
top_n: int = typer.Option(10, "--top", "-n", help="Top N results"),
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Scan for trading opportunities."""
|
|
183
|
+
from clawquant.cli.radar_cli import scan
|
|
184
|
+
scan(symbols=symbols, strategies=strategies, interval=interval, days=days,
|
|
185
|
+
exchange=exchange, top_n=top_n, json_mode=_get_json_mode())
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@radar_app.command("explain")
|
|
189
|
+
def radar_explain(
|
|
190
|
+
symbol: str = typer.Argument(..., help="Symbol to explain"),
|
|
191
|
+
strategy: str = typer.Argument(..., help="Strategy name"),
|
|
192
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
193
|
+
days: int = typer.Option(10, "--days", "-d", help="Data window"),
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Explain a specific opportunity."""
|
|
196
|
+
from clawquant.cli.radar_cli import explain
|
|
197
|
+
explain(symbol=symbol, strategy=strategy, interval=interval, days=days,
|
|
198
|
+
json_mode=_get_json_mode())
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
app.add_typer(radar_app, name="radar")
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Sub-app: report
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
report_app = typer.Typer(help="Report generation commands", no_args_is_help=True)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@report_app.command("generate")
|
|
210
|
+
def report_generate(
|
|
211
|
+
run_id: str = typer.Argument(..., help="Run ID to generate report for"),
|
|
212
|
+
formats: Optional[str] = typer.Option(None, "--formats", "-f", help="Comma-separated: json,md,charts"),
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Generate reports for a backtest run."""
|
|
215
|
+
from clawquant.cli.report_cli import generate
|
|
216
|
+
generate(run_id=run_id, formats=formats, json_mode=_get_json_mode())
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@report_app.command("batch")
|
|
220
|
+
def report_batch(
|
|
221
|
+
run_ids: str = typer.Argument(..., help="Comma-separated run IDs"),
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Generate and compare reports for multiple runs."""
|
|
224
|
+
from clawquant.cli.report_cli import batch_generate
|
|
225
|
+
batch_generate(run_ids=run_ids, json_mode=_get_json_mode())
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
app.add_typer(report_app, name="report")
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Sub-app: deploy
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
deploy_app = typer.Typer(help="Deployment commands", no_args_is_help=True)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@deploy_app.command("paper")
|
|
237
|
+
def deploy_paper(
|
|
238
|
+
strategy: str = typer.Argument(..., help="Strategy name"),
|
|
239
|
+
symbol: str = typer.Option("BTC/USDT", "--symbol", "-s", help="Trading pair"),
|
|
240
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
241
|
+
capital: float = typer.Option(10000.0, "--capital", "-c", help="Initial capital"),
|
|
242
|
+
params: Optional[str] = typer.Option(None, "--params", "-p", help="Strategy params JSON"),
|
|
243
|
+
exchange: str = typer.Option("binance", "--exchange", "-e", help="Exchange"),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Start paper trading."""
|
|
246
|
+
from clawquant.cli.deploy_cli import paper
|
|
247
|
+
paper(strategy=strategy, symbol=symbol, interval=interval, capital=capital,
|
|
248
|
+
params=params, exchange=exchange, json_mode=_get_json_mode())
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@deploy_app.command("live")
|
|
252
|
+
def deploy_live(
|
|
253
|
+
strategy: str = typer.Argument(..., help="Strategy name"),
|
|
254
|
+
symbol: str = typer.Option("BTC/USDT", "--symbol", "-s", help="Trading pair"),
|
|
255
|
+
interval: str = typer.Option("1h", "--interval", "-i", help="Bar interval"),
|
|
256
|
+
capital: float = typer.Option(10000.0, "--capital", "-c", help="Initial capital"),
|
|
257
|
+
params: Optional[str] = typer.Option(None, "--params", "-p", help="Strategy params JSON"),
|
|
258
|
+
exchange: str = typer.Option("binance", "--exchange", "-e", help="Exchange"),
|
|
259
|
+
i_know_what_im_doing: bool = typer.Option(False, "--i-know-what-im-doing", help="Confirm live trading risks"),
|
|
260
|
+
) -> None:
|
|
261
|
+
"""Start live trading (requires --i-know-what-im-doing)."""
|
|
262
|
+
from clawquant.cli.deploy_cli import live
|
|
263
|
+
live(strategy=strategy, symbol=symbol, interval=interval, capital=capital,
|
|
264
|
+
params=params, exchange=exchange, confirm=i_know_what_im_doing,
|
|
265
|
+
json_mode=_get_json_mode())
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@deploy_app.command("status")
|
|
269
|
+
def deploy_status() -> None:
|
|
270
|
+
"""Show deployment statuses."""
|
|
271
|
+
from clawquant.cli.deploy_cli import status
|
|
272
|
+
status(json_mode=_get_json_mode())
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@deploy_app.command("stop")
|
|
276
|
+
def deploy_stop(
|
|
277
|
+
strategy: str = typer.Argument(..., help="Strategy name"),
|
|
278
|
+
symbol: str = typer.Option("BTC/USDT", "--symbol", "-s", help="Trading pair"),
|
|
279
|
+
mode: str = typer.Option("paper", "--mode", "-m", help="paper or live"),
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Stop a deployment."""
|
|
282
|
+
from clawquant.cli.deploy_cli import stop
|
|
283
|
+
stop(strategy=strategy, symbol=symbol, mode=mode, json_mode=_get_json_mode())
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@deploy_app.command("flatten")
|
|
287
|
+
def deploy_flatten(
|
|
288
|
+
strategy: str = typer.Argument(..., help="Strategy name"),
|
|
289
|
+
symbol: str = typer.Option("BTC/USDT", "--symbol", "-s", help="Trading pair"),
|
|
290
|
+
mode: str = typer.Option("paper", "--mode", "-m", help="paper or live"),
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Flatten positions and stop a deployment."""
|
|
293
|
+
from clawquant.cli.deploy_cli import flatten
|
|
294
|
+
flatten(strategy=strategy, symbol=symbol, mode=mode, json_mode=_get_json_mode())
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
app.add_typer(deploy_app, name="deploy")
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Module entry-point
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Backtest CLI command implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from clawquant.core.utils.output import print_error, print_result, print_table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
strategy: str,
|
|
13
|
+
symbol: str = "BTC/USDT",
|
|
14
|
+
interval: str = "1h",
|
|
15
|
+
days: int = 30,
|
|
16
|
+
capital: float = 10000.0,
|
|
17
|
+
fee_bps: int = 10,
|
|
18
|
+
slippage_bps: int = 5,
|
|
19
|
+
params: Optional[str] = None,
|
|
20
|
+
dry_run: bool = False,
|
|
21
|
+
json_mode: bool = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Run a single backtest."""
|
|
24
|
+
from clawquant.core.backtest.config import BacktestConfig
|
|
25
|
+
from clawquant.core.backtest.engine import BacktestEngine
|
|
26
|
+
from clawquant.core.data.fetcher import fetch_data
|
|
27
|
+
from clawquant.core.data.models import DataPullRequest
|
|
28
|
+
from clawquant.core.runtime.loader import load_strategy
|
|
29
|
+
|
|
30
|
+
# Parse strategy params
|
|
31
|
+
strategy_params = {}
|
|
32
|
+
if params:
|
|
33
|
+
try:
|
|
34
|
+
strategy_params = json.loads(params)
|
|
35
|
+
except json.JSONDecodeError as e:
|
|
36
|
+
print_error("ConfigError", f"Invalid params JSON: {e}", "Use valid JSON, e.g. '{\"fast_period\": 10}'")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Load strategy
|
|
40
|
+
try:
|
|
41
|
+
strat = load_strategy(strategy)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print_error("StrategyError", f"Failed to load strategy '{strategy}': {e}", "Check strategy name with 'clawquant strategy list'")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Build config
|
|
47
|
+
config = BacktestConfig(
|
|
48
|
+
initial_capital=capital,
|
|
49
|
+
fee_bps=fee_bps,
|
|
50
|
+
slippage_bps=slippage_bps,
|
|
51
|
+
strategy_name=strategy,
|
|
52
|
+
strategy_params=strategy_params,
|
|
53
|
+
symbol=symbol,
|
|
54
|
+
interval=interval,
|
|
55
|
+
days=days,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Fetch data
|
|
59
|
+
try:
|
|
60
|
+
request = DataPullRequest(symbols=[symbol], interval=interval, days=days)
|
|
61
|
+
dfs = fetch_data(request)
|
|
62
|
+
df = dfs.get(symbol)
|
|
63
|
+
if df is None or df.empty:
|
|
64
|
+
print_error("DataError", f"No data for {symbol}", "Try 'clawquant data pull' first")
|
|
65
|
+
return
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print_error("DataError", f"Failed to fetch data: {e}", "Check network or try 'clawquant data pull' first")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Dry run check
|
|
71
|
+
if dry_run:
|
|
72
|
+
from clawquant.core.data.inspector import inspect_data
|
|
73
|
+
report = inspect_data(df, symbol, interval)
|
|
74
|
+
meta = strat.metadata()
|
|
75
|
+
result_data = {
|
|
76
|
+
"ready": report.passed,
|
|
77
|
+
"strategy": meta.name,
|
|
78
|
+
"data_bars": report.total_bars,
|
|
79
|
+
"data_quality": "PASS" if report.passed else "FAIL",
|
|
80
|
+
"issues": report.issues,
|
|
81
|
+
}
|
|
82
|
+
print_result(result_data, json_mode=json_mode)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Run backtest
|
|
86
|
+
engine = BacktestEngine(config, strat, df)
|
|
87
|
+
result = engine.run()
|
|
88
|
+
|
|
89
|
+
if not result.success:
|
|
90
|
+
print_error(result.error_type or "Error", result.message or "Unknown error")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Display results
|
|
94
|
+
summary = {
|
|
95
|
+
"run_id": result.run_id,
|
|
96
|
+
"strategy": strategy,
|
|
97
|
+
"symbol": symbol,
|
|
98
|
+
"total_return": f"{result.total_return_pct:.2f}%",
|
|
99
|
+
"max_drawdown": f"{result.max_drawdown_pct:.2f}%",
|
|
100
|
+
"sharpe_ratio": f"{result.sharpe_ratio:.4f}",
|
|
101
|
+
"win_rate": f"{result.win_rate:.1f}%",
|
|
102
|
+
"total_trades": result.total_trades,
|
|
103
|
+
"profit_factor": f"{result.profit_factor:.4f}",
|
|
104
|
+
"avg_trade_pnl": f"${result.avg_trade_pnl:.2f}",
|
|
105
|
+
"warnings": result.warnings,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if json_mode:
|
|
109
|
+
print_result(result.model_dump(mode="json"), json_mode=True)
|
|
110
|
+
else:
|
|
111
|
+
print_table(
|
|
112
|
+
headers=["Metric", "Value"],
|
|
113
|
+
rows=[[k, str(v)] for k, v in summary.items() if k != "warnings"],
|
|
114
|
+
title=f"Backtest Result: {result.run_id}",
|
|
115
|
+
json_mode=False,
|
|
116
|
+
)
|
|
117
|
+
if result.warnings:
|
|
118
|
+
from rich.console import Console
|
|
119
|
+
console = Console()
|
|
120
|
+
for w in result.warnings:
|
|
121
|
+
console.print(f" [yellow]⚠ {w}[/yellow]")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def batch(
|
|
125
|
+
strategies: str,
|
|
126
|
+
symbols: str,
|
|
127
|
+
interval: str = "1h",
|
|
128
|
+
days: int = 30,
|
|
129
|
+
capital: float = 10000.0,
|
|
130
|
+
json_mode: bool = False,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Run batch backtest (delegates to batch module)."""
|
|
133
|
+
from clawquant.core.backtest.batch import run_batch
|
|
134
|
+
|
|
135
|
+
strategy_list = [s.strip() for s in strategies.split(",")]
|
|
136
|
+
symbol_list = [s.strip() for s in symbols.split(",")]
|
|
137
|
+
|
|
138
|
+
results = run_batch(strategy_list, symbol_list, interval, days, capital)
|
|
139
|
+
|
|
140
|
+
if json_mode:
|
|
141
|
+
print_result([r.model_dump(mode="json") for r in results], json_mode=True)
|
|
142
|
+
else:
|
|
143
|
+
rows = []
|
|
144
|
+
for r in results:
|
|
145
|
+
rows.append([
|
|
146
|
+
r.run_id[:40],
|
|
147
|
+
r.run_meta.strategy["name"] if r.run_meta else "?",
|
|
148
|
+
r.run_meta.data["symbol"] if r.run_meta else "?",
|
|
149
|
+
f"{r.total_return_pct:.2f}%",
|
|
150
|
+
f"{r.max_drawdown_pct:.2f}%",
|
|
151
|
+
str(r.total_trades),
|
|
152
|
+
f"{r.win_rate:.1f}%",
|
|
153
|
+
])
|
|
154
|
+
print_table(
|
|
155
|
+
headers=["Run ID", "Strategy", "Symbol", "Return", "MaxDD", "Trades", "WinRate"],
|
|
156
|
+
rows=rows,
|
|
157
|
+
title="Batch Backtest Results",
|
|
158
|
+
json_mode=False,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def sweep(
|
|
163
|
+
strategy: str,
|
|
164
|
+
symbol: str = "BTC/USDT",
|
|
165
|
+
interval: str = "1h",
|
|
166
|
+
days: int = 30,
|
|
167
|
+
param_grid: Optional[str] = None,
|
|
168
|
+
mode: str = "grid",
|
|
169
|
+
n_random: int = 20,
|
|
170
|
+
json_mode: bool = False,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Run parameter sweep (delegates to sweep module)."""
|
|
173
|
+
from clawquant.core.backtest.sweep import run_sweep
|
|
174
|
+
|
|
175
|
+
grid = {}
|
|
176
|
+
if param_grid:
|
|
177
|
+
try:
|
|
178
|
+
grid = json.loads(param_grid)
|
|
179
|
+
except json.JSONDecodeError as e:
|
|
180
|
+
print_error("ConfigError", f"Invalid param_grid JSON: {e}")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
results = run_sweep(strategy, symbol, interval, days, grid, mode, n_random)
|
|
184
|
+
|
|
185
|
+
if json_mode:
|
|
186
|
+
print_result([r.model_dump(mode="json") for r in results], json_mode=True)
|
|
187
|
+
else:
|
|
188
|
+
rows = []
|
|
189
|
+
for r in results:
|
|
190
|
+
params_str = json.dumps(r.run_meta.strategy["params"]) if r.run_meta else "?"
|
|
191
|
+
rows.append([
|
|
192
|
+
params_str[:50],
|
|
193
|
+
f"{r.total_return_pct:.2f}%",
|
|
194
|
+
f"{r.max_drawdown_pct:.2f}%",
|
|
195
|
+
str(r.total_trades),
|
|
196
|
+
f"{r.win_rate:.1f}%",
|
|
197
|
+
f"{r.stability_score:.1f}",
|
|
198
|
+
])
|
|
199
|
+
print_table(
|
|
200
|
+
headers=["Params", "Return", "MaxDD", "Trades", "WinRate", "Score"],
|
|
201
|
+
rows=rows,
|
|
202
|
+
title=f"Parameter Sweep: {strategy} on {symbol}",
|
|
203
|
+
json_mode=False,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def walkforward(
|
|
208
|
+
strategy: str,
|
|
209
|
+
symbol: str = "BTC/USDT",
|
|
210
|
+
interval: str = "1h",
|
|
211
|
+
days: int = 90,
|
|
212
|
+
train_pct: float = 0.7,
|
|
213
|
+
n_splits: int = 3,
|
|
214
|
+
param_grid: Optional[str] = None,
|
|
215
|
+
json_mode: bool = False,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Run walk-forward validation (delegates to walkforward module)."""
|
|
218
|
+
from clawquant.core.backtest.walkforward import run_walkforward
|
|
219
|
+
|
|
220
|
+
grid = {}
|
|
221
|
+
if param_grid:
|
|
222
|
+
try:
|
|
223
|
+
grid = json.loads(param_grid)
|
|
224
|
+
except json.JSONDecodeError as e:
|
|
225
|
+
print_error("ConfigError", f"Invalid param_grid JSON: {e}")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
results = run_walkforward(strategy, symbol, interval, days, train_pct, n_splits, grid)
|
|
229
|
+
|
|
230
|
+
if json_mode:
|
|
231
|
+
print_result(results, json_mode=True)
|
|
232
|
+
else:
|
|
233
|
+
print_result(results, json_mode=False)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""CLI commands for the ``data`` sub-app.
|
|
2
|
+
|
|
3
|
+
These functions are wired into the main Typer application in
|
|
4
|
+
:pymod:`clawquant.clawquant_cli`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from clawquant.core.utils.output import print_error, print_result, print_table
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pull(
|
|
15
|
+
symbols: str,
|
|
16
|
+
interval: str,
|
|
17
|
+
days: int,
|
|
18
|
+
exchange: str,
|
|
19
|
+
json_mode: bool,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Pull OHLCV data for one or more symbols."""
|
|
22
|
+
from clawquant.core.data.fetcher import fetch_data
|
|
23
|
+
from clawquant.core.data.models import DataPullRequest
|
|
24
|
+
|
|
25
|
+
symbol_list = [s.strip() for s in symbols.split(",")]
|
|
26
|
+
typer.echo(
|
|
27
|
+
f"[data pull] Fetching {symbol_list} | interval={interval} "
|
|
28
|
+
f"| days={days} | exchange={exchange}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
request = DataPullRequest(
|
|
32
|
+
symbols=symbol_list,
|
|
33
|
+
interval=interval,
|
|
34
|
+
days=days,
|
|
35
|
+
exchange=exchange,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
result = fetch_data(request)
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
print_error("FetchError", str(exc), suggestion="Check your network or API keys.")
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
|
|
44
|
+
rows = []
|
|
45
|
+
for sym, df in result.items():
|
|
46
|
+
bar_count = len(df)
|
|
47
|
+
start = str(df["timestamp"].min()) if bar_count else "-"
|
|
48
|
+
end = str(df["timestamp"].max()) if bar_count else "-"
|
|
49
|
+
rows.append((sym, interval, bar_count, start, end))
|
|
50
|
+
|
|
51
|
+
print_table(
|
|
52
|
+
headers=["Symbol", "Interval", "Bars", "Start", "End"],
|
|
53
|
+
rows=rows,
|
|
54
|
+
title="Data Pull Results",
|
|
55
|
+
json_mode=json_mode,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def inspect(
|
|
60
|
+
symbol: str,
|
|
61
|
+
interval: str,
|
|
62
|
+
json_mode: bool,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Run data quality checks on cached data for a single symbol."""
|
|
65
|
+
from clawquant.core.data.cache import read_cache
|
|
66
|
+
from clawquant.core.data.inspector import inspect_data
|
|
67
|
+
|
|
68
|
+
typer.echo(f"[data inspect] Checking {symbol} @ {interval} ...")
|
|
69
|
+
|
|
70
|
+
df = read_cache(symbol, interval)
|
|
71
|
+
if df is None or df.empty:
|
|
72
|
+
print_error(
|
|
73
|
+
"DataNotFound",
|
|
74
|
+
f"No cached data for {symbol}/{interval}.",
|
|
75
|
+
suggestion="Run 'clawquant data pull' first.",
|
|
76
|
+
)
|
|
77
|
+
raise typer.Exit(code=1)
|
|
78
|
+
|
|
79
|
+
report = inspect_data(df, symbol, interval)
|
|
80
|
+
print_result(report, json_mode=json_mode)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cache_status(json_mode: bool) -> None:
|
|
84
|
+
"""Show information about all cached data files."""
|
|
85
|
+
from clawquant.core.data.cache import cache_status as _cache_status
|
|
86
|
+
|
|
87
|
+
entries = _cache_status()
|
|
88
|
+
if not entries:
|
|
89
|
+
typer.echo("[data cache-status] Cache is empty.")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
rows = [
|
|
93
|
+
(e["file"], e["rows"], e["start"], e["end"], e["size_kb"])
|
|
94
|
+
for e in entries
|
|
95
|
+
]
|
|
96
|
+
print_table(
|
|
97
|
+
headers=["File", "Rows", "Start", "End", "Size (KB)"],
|
|
98
|
+
rows=rows,
|
|
99
|
+
title="Cached Datasets",
|
|
100
|
+
json_mode=json_mode,
|
|
101
|
+
)
|