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.
Files changed (66) hide show
  1. clawquant/__init__.py +1 -0
  2. clawquant/clawquant_cli.py +303 -0
  3. clawquant/cli/__init__.py +0 -0
  4. clawquant/cli/backtest_cli.py +233 -0
  5. clawquant/cli/data_cli.py +101 -0
  6. clawquant/cli/deploy_cli.py +154 -0
  7. clawquant/cli/radar_cli.py +98 -0
  8. clawquant/cli/report_cli.py +80 -0
  9. clawquant/cli/strategy_cli.py +215 -0
  10. clawquant/core/__init__.py +0 -0
  11. clawquant/core/backtest/__init__.py +0 -0
  12. clawquant/core/backtest/batch.py +119 -0
  13. clawquant/core/backtest/config.py +36 -0
  14. clawquant/core/backtest/engine.py +346 -0
  15. clawquant/core/backtest/events.py +58 -0
  16. clawquant/core/backtest/execution.py +67 -0
  17. clawquant/core/backtest/portfolio.py +170 -0
  18. clawquant/core/backtest/result.py +69 -0
  19. clawquant/core/backtest/risk.py +85 -0
  20. clawquant/core/backtest/sweep.py +130 -0
  21. clawquant/core/backtest/walkforward.py +169 -0
  22. clawquant/core/data/__init__.py +0 -0
  23. clawquant/core/data/alignment.py +91 -0
  24. clawquant/core/data/cache.py +120 -0
  25. clawquant/core/data/fetcher.py +150 -0
  26. clawquant/core/data/inspector.py +119 -0
  27. clawquant/core/data/models.py +59 -0
  28. clawquant/core/deploy/__init__.py +0 -0
  29. clawquant/core/deploy/manager.py +72 -0
  30. clawquant/core/deploy/runner.py +196 -0
  31. clawquant/core/evaluate/__init__.py +0 -0
  32. clawquant/core/evaluate/metrics.py +154 -0
  33. clawquant/core/evaluate/scorer.py +137 -0
  34. clawquant/core/radar/__init__.py +0 -0
  35. clawquant/core/radar/explainer.py +89 -0
  36. clawquant/core/radar/scanner.py +150 -0
  37. clawquant/core/report/__init__.py +0 -0
  38. clawquant/core/report/charts.py +133 -0
  39. clawquant/core/report/generator.py +155 -0
  40. clawquant/core/report/json_report.py +40 -0
  41. clawquant/core/report/markdown_report.py +127 -0
  42. clawquant/core/runtime/__init__.py +0 -0
  43. clawquant/core/runtime/base_strategy.py +116 -0
  44. clawquant/core/runtime/loader.py +370 -0
  45. clawquant/core/runtime/models.py +45 -0
  46. clawquant/core/runtime/sandbox.py +88 -0
  47. clawquant/core/utils/__init__.py +0 -0
  48. clawquant/core/utils/logging.py +64 -0
  49. clawquant/core/utils/output.py +104 -0
  50. clawquant/core/utils/run_id.py +69 -0
  51. clawquant/core/utils/state.py +12 -0
  52. clawquant/integrations/__init__.py +0 -0
  53. clawquant/integrations/binance_skill_wrapper/__init__.py +0 -0
  54. clawquant/integrations/binance_skill_wrapper/wrapper.py +75 -0
  55. clawquant/integrations/ccxt_fallback/__init__.py +0 -0
  56. clawquant/integrations/ccxt_fallback/client.py +154 -0
  57. clawquant/strategies_builtin/__init__.py +11 -0
  58. clawquant/strategies_builtin/dca.py +149 -0
  59. clawquant/strategies_builtin/grid.py +249 -0
  60. clawquant/strategies_builtin/ma_crossover.py +202 -0
  61. clawquant-0.1.0.dist-info/METADATA +170 -0
  62. clawquant-0.1.0.dist-info/RECORD +66 -0
  63. clawquant-0.1.0.dist-info/WHEEL +5 -0
  64. clawquant-0.1.0.dist-info/entry_points.txt +2 -0
  65. clawquant-0.1.0.dist-info/licenses/LICENSE +21 -0
  66. 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
+ )