quantpipe 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.
quantpipe/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """QuantPipe - Quantitative trading toolkit for LLM AI."""
2
+
3
+ __version__ = "0.1.0"
quantpipe/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ """Entry point for python -m quantpipe."""
3
+
4
+ from .cli import cli
5
+
6
+ if __name__ == '__main__':
7
+ cli()
quantpipe/cli.py ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env python3
2
+ """CLI entry point for QuantPipe."""
3
+
4
+ import click
5
+
6
+ from .cli_exit import EXIT_CODE_HELP
7
+ from .commands.backtest import backtest_command
8
+ from .commands.signal import signal_command
9
+ from .commands.scan import scan_command
10
+ from .commands.paper import paper_command
11
+
12
+
13
+ @click.group(epilog=EXIT_CODE_HELP)
14
+ @click.version_option(version='0.1.0', prog_name='quantpipe')
15
+ def cli():
16
+ """QuantPipe - Quantitative trading toolkit for LLM AI.
17
+
18
+ Designed to work seamlessly with seamflux CLI for market data via Unix pipes.
19
+
20
+ Auto-detects data formats: Binance, Polymarket, Uniswap, generic OHLCV.
21
+
22
+ Examples:
23
+ # Generate signal from Binance
24
+ seamflux invoke binance fetchOhlcv -p symbol=BTC/USDT -p timeframe=1h | quantpipe signal --strategy ma_cross --stdin
25
+
26
+ # Generate signal from Polymarket
27
+ seamflux invoke polymarket getPriceHistory -p market=<token_id> -p interval=1h | quantpipe signal --strategy rsi --stdin
28
+
29
+ # Run backtest
30
+ quantpipe backtest --data btc_history.json --strategy rsi --json
31
+
32
+ # Scan multiple symbols
33
+ seamflux invoke binance fetchTickers | quantpipe scan --strategy ma_cross --filter-signal BUY
34
+
35
+ # Paper trading with Polymarket
36
+ quantpipe paper --exec "seamflux invoke polymarket getMarket -p slug=<market_slug>" --strategy ma_cross
37
+ """
38
+ pass
39
+
40
+
41
+ # Register commands
42
+ cli.add_command(backtest_command)
43
+ cli.add_command(signal_command)
44
+ cli.add_command(scan_command)
45
+ cli.add_command(paper_command)
46
+
47
+
48
+ if __name__ == '__main__':
49
+ cli()
quantpipe/cli_exit.py ADDED
@@ -0,0 +1,15 @@
1
+ """Process exit codes aligned with repo rule.md (§3.3)."""
2
+
3
+ # 0 success; 1 generic (args, IO); 2 input/schema mismatch; 3 upstream unavailable
4
+ EXIT_OK = 0
5
+ EXIT_GENERAL = 1
6
+ EXIT_SCHEMA = 2
7
+ EXIT_UPSTREAM = 3
8
+
9
+ EXIT_CODE_HELP = """
10
+ Exit codes:
11
+ 0 Success
12
+ 1 General failure (invalid arguments, I/O error)
13
+ 2 Input format / schema mismatch
14
+ 3 Upstream dependency unavailable (e.g. subprocess failed, missing external tool)
15
+ """
@@ -0,0 +1 @@
1
+ """CLI commands for QuantPipe."""
@@ -0,0 +1,285 @@
1
+ """Backtest command for QuantPipe."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+ from typing import Any, Dict, Optional
8
+
9
+ import click
10
+ import pandas as pd
11
+
12
+ from ..cli_exit import EXIT_GENERAL, EXIT_SCHEMA
13
+ from ..core.backtest import BacktestEngine
14
+ from ..core.data import prepare_data
15
+ from ..core.pipeline_json import emit_json_line, failure_envelope, success_envelope, utc_now_iso
16
+ from ..strategies import STRATEGIES
17
+ from .io_util import QuantpipeCliError, configure_verbose, load_cli_input_dict
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ def _parse_strategy_params(params: tuple) -> dict:
23
+ strategy_params = {}
24
+ for param in params:
25
+ if "=" not in param:
26
+ raise QuantpipeCliError(EXIT_GENERAL, f"Invalid param format '{param}'. Use key=value", "INVALID_PARAM")
27
+ key, value = param.split("=", 1)
28
+ try:
29
+ if "." in value:
30
+ value = float(value)
31
+ else:
32
+ value = int(value)
33
+ except ValueError:
34
+ pass
35
+ strategy_params[key] = value
36
+ return strategy_params
37
+
38
+
39
+ def _resolve_run_output_dir(run_dir: Optional[str], run_id: Optional[str]) -> Optional[str]:
40
+ if run_dir:
41
+ return os.path.abspath(run_dir)
42
+ if run_id:
43
+ return os.path.abspath(os.path.join(os.getcwd(), "runs", run_id))
44
+ return None
45
+
46
+
47
+ def _artifact_rel_paths(files: Dict[str, str]) -> Dict[str, str]:
48
+ cwd = os.getcwd()
49
+ out: Dict[str, str] = {}
50
+ for k, p in files.items():
51
+ try:
52
+ out[k] = os.path.relpath(p, cwd)
53
+ except ValueError:
54
+ out[k] = p
55
+ return out
56
+
57
+
58
+ def _write_equity_csv(engine: BacktestEngine, path: str) -> None:
59
+ idx = engine.df.index
60
+ eq = engine.equity_curve
61
+ if len(idx) == 0:
62
+ pd.DataFrame(columns=["timestamp", "equity"]).to_csv(path, index=False)
63
+ return
64
+ if len(eq) != len(idx):
65
+ n = min(len(idx), len(eq))
66
+ idx = idx[:n]
67
+ eq = eq[:n]
68
+ df = pd.DataFrame({"timestamp": idx, "equity": eq})
69
+ df.to_csv(path, index=False)
70
+
71
+
72
+ def _write_trades_csv(engine: BacktestEngine, path: str) -> None:
73
+ rows = []
74
+ for t in engine.trades:
75
+ rows.append(
76
+ {
77
+ "timestamp": t.exit_time.isoformat(),
78
+ "entry_time": t.entry_time.isoformat(),
79
+ "side": t.side,
80
+ "price": t.exit_price,
81
+ "entry_price": t.entry_price,
82
+ "exit_price": t.exit_price,
83
+ "size": t.size,
84
+ "pnl": t.pnl,
85
+ "pnl_pct": t.pnl_pct,
86
+ }
87
+ )
88
+ pd.DataFrame(rows).to_csv(path, index=False)
89
+
90
+
91
+ def _package_version() -> str:
92
+ try:
93
+ import importlib.metadata as im
94
+
95
+ return im.version("quantpipe")
96
+ except Exception:
97
+ return "0.1.0"
98
+
99
+
100
+ @click.command(name="backtest")
101
+ @click.option("--data", "-d", "data_path", type=str, default=None, help="Data file path (JSON)")
102
+ @click.option("--input", "-i", "input_path", type=str, default=None, help="Alias for --data")
103
+ @click.option("--stdin", "use_stdin", is_flag=True, help="Read data from stdin")
104
+ @click.option("--strategy", "-s", required=True, help="Strategy name")
105
+ @click.option("--param", "params", multiple=True, help="Strategy parameters (key=value)")
106
+ @click.option("--json", "output_json", is_flag=True, help="Machine-readable JSON envelope on stdout")
107
+ @click.option("--pretty", is_flag=True, help="Pretty-print JSON (not for pipes)")
108
+ @click.option("--verbose", "-v", is_flag=True, help="Debug logs on stderr")
109
+ @click.option("--dry-run", is_flag=True, help="Validate input and strategy; do not run backtest or write files")
110
+ @click.option(
111
+ "--run-dir",
112
+ type=click.Path(file_okay=False, dir_okay=True, writable=True, path_type=str),
113
+ default=None,
114
+ help="Write trades.csv, equity.csv, summary.json, manifest.json under this directory",
115
+ )
116
+ @click.option("--run-id", type=str, default=None, help="Write artifacts to runs/<run-id>/ (ignored if --run-dir set)")
117
+ def backtest_command(
118
+ data_path: Optional[str],
119
+ input_path: Optional[str],
120
+ use_stdin: bool,
121
+ strategy: str,
122
+ params: tuple,
123
+ output_json: bool,
124
+ pretty: bool,
125
+ verbose: bool,
126
+ dry_run: bool,
127
+ run_dir: Optional[str],
128
+ run_id: Optional[str],
129
+ ):
130
+ """Run backtest on historical data."""
131
+ configure_verbose(verbose)
132
+ command = "backtest"
133
+ path = input_path or data_path
134
+
135
+ try:
136
+ strategy_params = _parse_strategy_params(params)
137
+ except QuantpipeCliError as e:
138
+ click.echo(e.message, err=True)
139
+ if output_json or pretty:
140
+ emit_json_line(failure_envelope(command, e.error_code, e.message), pretty=pretty)
141
+ sys.exit(e.exit_code)
142
+
143
+ try:
144
+ input_data = load_cli_input_dict(use_stdin=use_stdin, path=path)
145
+ except QuantpipeCliError as e:
146
+ click.echo(e.message, err=True)
147
+ if output_json or pretty:
148
+ emit_json_line(failure_envelope(command, e.error_code, e.message), pretty=pretty)
149
+ sys.exit(e.exit_code)
150
+
151
+ try:
152
+ df, metadata = prepare_data(input_data)
153
+ except Exception as e:
154
+ click.echo(f"Error preparing data: {e}", err=True)
155
+ if output_json or pretty:
156
+ emit_json_line(failure_envelope(command, "INVALID_INPUT", str(e)), pretty=pretty)
157
+ sys.exit(EXIT_SCHEMA)
158
+
159
+ if strategy not in STRATEGIES:
160
+ msg = f"Unknown strategy '{strategy}'. Available: {', '.join(STRATEGIES.keys())}"
161
+ click.echo(f"Error: {msg}", err=True)
162
+ if output_json or pretty:
163
+ emit_json_line(failure_envelope(command, "INVALID_INPUT", msg), pretty=pretty)
164
+ sys.exit(EXIT_GENERAL)
165
+
166
+ StrategyClass = STRATEGIES[strategy]
167
+ strat = StrategyClass(params=strategy_params)
168
+
169
+ started = utc_now_iso()
170
+ out_dir = _resolve_run_output_dir(run_dir, run_id)
171
+ effective_run_id = run_id if run_id else None
172
+
173
+ data_body: Dict[str, Any] = {
174
+ "source": {
175
+ "symbol": metadata.get("symbol"),
176
+ "market_type": metadata.get("market_type", "unknown"),
177
+ "type": metadata.get("type"),
178
+ "bars": len(df),
179
+ "period": {
180
+ "start": metadata.get("start"),
181
+ "end": metadata.get("end"),
182
+ },
183
+ },
184
+ "strategy": {"name": strategy, "params": strat.params},
185
+ }
186
+
187
+ if dry_run:
188
+ data_body["dryRun"] = True
189
+ if output_json or pretty:
190
+ emit_json_line(
191
+ success_envelope(
192
+ command,
193
+ data_body,
194
+ run_id=effective_run_id,
195
+ meta={"startedAt": started, "finishedAt": utc_now_iso()},
196
+ ),
197
+ pretty=pretty,
198
+ )
199
+ else:
200
+ click.echo("\nDry run OK")
201
+ click.echo(f"Strategy: {strategy} {strat.params}")
202
+ click.echo(f"Bars: {len(df)}")
203
+ return
204
+
205
+ engine = BacktestEngine(strat, df)
206
+ metrics = engine.run()
207
+ data_body["performance"] = metrics
208
+
209
+ artifacts_abs: Dict[str, str] = {}
210
+ if out_dir:
211
+ os.makedirs(out_dir, exist_ok=True)
212
+ trades_p = os.path.join(out_dir, "trades.csv")
213
+ equity_p = os.path.join(out_dir, "equity.csv")
214
+ summary_p = os.path.join(out_dir, "summary.json")
215
+ manifest_p = os.path.join(out_dir, "manifest.json")
216
+
217
+ _write_trades_csv(engine, trades_p)
218
+ _write_equity_csv(engine, equity_p)
219
+ artifacts_abs["trades"] = trades_p
220
+ artifacts_abs["equity"] = equity_p
221
+
222
+ summary_payload = dict(data_body)
223
+ with open(summary_p, "w", encoding="utf-8") as f:
224
+ json.dump(summary_payload, f, indent=2, default=str)
225
+ artifacts_abs["summary"] = summary_p
226
+
227
+ manifest = {
228
+ "schemaVersion": 1,
229
+ "tool": "quantpipe",
230
+ "command": command,
231
+ "quantpipeVersion": _package_version(),
232
+ "strategy": strategy,
233
+ "strategyParams": strat.params,
234
+ "input": {"stdin": use_stdin, "path": path},
235
+ "runId": effective_run_id,
236
+ }
237
+ with open(manifest_p, "w", encoding="utf-8") as f:
238
+ json.dump(manifest, f, indent=2, default=str)
239
+ artifacts_abs["manifest"] = manifest_p
240
+ log.debug("Wrote artifacts under %s", out_dir)
241
+
242
+ finished = utc_now_iso()
243
+ artifacts_rel = _artifact_rel_paths(artifacts_abs) if artifacts_abs else None
244
+
245
+ if output_json or pretty:
246
+ emit_json_line(
247
+ success_envelope(
248
+ command,
249
+ data_body,
250
+ run_id=effective_run_id,
251
+ artifacts=artifacts_rel,
252
+ meta={"startedAt": started, "finishedAt": finished},
253
+ ),
254
+ pretty=pretty,
255
+ )
256
+ else:
257
+ click.echo(f"\nBacktest Results")
258
+ click.echo(f"================")
259
+ click.echo(f"Strategy: {strategy} {strat.params}")
260
+ click.echo(f"Data Format: {metadata.get('format', 'unknown')}")
261
+ click.echo(f"Market Type: {metadata.get('market_type', 'unknown')}")
262
+ click.echo(f"Symbol: {metadata.get('symbol', 'N/A')}")
263
+ click.echo(f"Period: {metadata.get('start')} to {metadata.get('end')}")
264
+ click.echo(f"Bars: {len(df)}")
265
+ if metadata.get("synthesized"):
266
+ click.echo(
267
+ f"Synthesized: {metadata.get('original_points')} price points -> OHLCV ({metadata.get('interval')})"
268
+ )
269
+ click.echo(f"")
270
+ click.echo(f"Performance:")
271
+ click.echo(f" Total Trades: {metrics['total_trades']}")
272
+ click.echo(f" Win Rate: {metrics['win_rate']:.2%}")
273
+ click.echo(f" Total Return: {metrics['total_return']:.2%}")
274
+ click.echo(f" Max Drawdown: {metrics['max_drawdown']:.2%}")
275
+ click.echo(f" Sharpe Ratio: {metrics['sharpe_ratio']:.2f}")
276
+ pf = metrics.get("profit_factor")
277
+ if pf is not None:
278
+ click.echo(f" Profit Factor: {pf:.2f}")
279
+ else:
280
+ click.echo(f" Profit Factor: N/A (no losing trades)")
281
+ if artifacts_abs:
282
+ click.echo("")
283
+ click.echo("Artifacts:")
284
+ for k, v in _artifact_rel_paths(artifacts_abs).items():
285
+ click.echo(f" {k}: {v}")
@@ -0,0 +1,60 @@
1
+ """Shared CLI input loading and logging setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import sys
8
+ from typing import Any, Dict, Optional
9
+
10
+ from ..cli_exit import EXIT_GENERAL, EXIT_SCHEMA
11
+ from ..core.pipeline_json import unwrap_upstream_json
12
+
13
+
14
+ class QuantpipeCliError(Exception):
15
+ """Raised for predictable CLI failures (carry exit code for rule.md)."""
16
+
17
+ def __init__(self, exit_code: int, message: str, error_code: str = "ERROR") -> None:
18
+ self.exit_code = exit_code
19
+ self.message = message
20
+ self.error_code = error_code
21
+ super().__init__(message)
22
+
23
+
24
+ def configure_verbose(verbose: bool) -> None:
25
+ """Route debug logs to stderr; avoid polluting stdout JSON."""
26
+ if verbose:
27
+ logging.basicConfig(
28
+ level=logging.DEBUG,
29
+ format="%(levelname)s %(message)s",
30
+ stream=sys.stderr,
31
+ force=True,
32
+ )
33
+ else:
34
+ logging.basicConfig(level=logging.WARNING, stream=sys.stderr, force=True)
35
+
36
+
37
+ def load_cli_input_dict(*, use_stdin: bool, path: Optional[str]) -> Dict[str, Any]:
38
+ """
39
+ Load JSON object from stdin or file; unwrap seamflux ``{"result": {...}}`` once.
40
+ """
41
+ try:
42
+ if use_stdin or path is None:
43
+ raw: Any = json.load(sys.stdin)
44
+ else:
45
+ with open(path, "r", encoding="utf-8") as f:
46
+ raw = json.load(f)
47
+ except FileNotFoundError as e:
48
+ raise QuantpipeCliError(EXIT_GENERAL, str(e), "IO_ERROR") from e
49
+ except json.JSONDecodeError as e:
50
+ raise QuantpipeCliError(EXIT_SCHEMA, f"Invalid JSON: {e}", "INVALID_JSON") from e
51
+ except OSError as e:
52
+ raise QuantpipeCliError(EXIT_GENERAL, f"I/O error: {e}", "IO_ERROR") from e
53
+
54
+ if not isinstance(raw, dict):
55
+ raise QuantpipeCliError(EXIT_SCHEMA, "Top-level JSON must be an object", "INVALID_INPUT")
56
+
57
+ unwrapped = unwrap_upstream_json(raw)
58
+ if not isinstance(unwrapped, dict):
59
+ raise QuantpipeCliError(EXIT_SCHEMA, "Unwrapped payload must be an object", "INVALID_INPUT")
60
+ return unwrapped