speculo 0.1.0__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.
speculo-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: speculo
3
+ Version: 0.1.0
4
+ Summary: Speculo Studio CLI — backtest your trading strategies locally and sync results.
5
+ Author: Speculo
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://speculotrading.com
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Speculo CLI
12
+
13
+ Run Speculo Studio backtests on your own machine — your hardware, your data, results
14
+ synced to your Studio history.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install speculo
20
+ ```
21
+
22
+ ## Authenticate
23
+
24
+ Create a CLI token in Studio → Settings → CLI tokens, then:
25
+
26
+ ```bash
27
+ speculo login --token spk_xxxxxxxxxxxx
28
+ # self-hosted / staging:
29
+ speculo login --token spk_xxx --api https://api.staging.speculotrading.com
30
+ ```
31
+
32
+ Credentials are stored in `~/.speculo/config.json` (mode 600). You can also set
33
+ `SPECULO_TOKEN` / `SPECULO_API` in the environment.
34
+
35
+ ## Backtest
36
+
37
+ ```bash
38
+ # real historical data (fetched via the platform's cached proxy)
39
+ speculo backtest strategy.py --symbol AAPL --timeframe 1d --capital 100000
40
+
41
+ # your own OHLCV CSV
42
+ speculo backtest strategy.py --csv mydata.csv
43
+
44
+ # synthetic demo series
45
+ speculo backtest strategy.py --bars 500
46
+ ```
47
+
48
+ Add `--no-sync` to keep a run private (compute locally, don't upload).
49
+
50
+ ## Strategy contract
51
+
52
+ A strategy defines `on_bar(ctx)` and optionally `initialize(ctx)` — identical to the
53
+ web editor:
54
+
55
+ ```python
56
+ def initialize(ctx):
57
+ ctx.state["fast"], ctx.state["slow"] = 5, 20
58
+
59
+ def on_bar(ctx):
60
+ c = ctx.closes()
61
+ if len(c) < ctx.state["slow"]:
62
+ return
63
+ fast = sum(c[-ctx.state["fast"]:]) / ctx.state["fast"]
64
+ slow = sum(c[-ctx.state["slow"]:]) / ctx.state["slow"]
65
+ if fast > slow and ctx.position == 0:
66
+ ctx.broker.place_order(symbol=ctx.symbol, quantity=10, side="BUY", order_type="MARKET")
67
+ elif fast < slow and ctx.position > 0:
68
+ ctx.broker.place_order(symbol=ctx.symbol, quantity=10, side="SELL", order_type="MARKET")
69
+ ```
70
+
71
+ The backtest engine itself is fetched from the platform at runtime, so the CLI always
72
+ runs the exact same engine as the web app.
@@ -0,0 +1,62 @@
1
+ # Speculo CLI
2
+
3
+ Run Speculo Studio backtests on your own machine — your hardware, your data, results
4
+ synced to your Studio history.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install speculo
10
+ ```
11
+
12
+ ## Authenticate
13
+
14
+ Create a CLI token in Studio → Settings → CLI tokens, then:
15
+
16
+ ```bash
17
+ speculo login --token spk_xxxxxxxxxxxx
18
+ # self-hosted / staging:
19
+ speculo login --token spk_xxx --api https://api.staging.speculotrading.com
20
+ ```
21
+
22
+ Credentials are stored in `~/.speculo/config.json` (mode 600). You can also set
23
+ `SPECULO_TOKEN` / `SPECULO_API` in the environment.
24
+
25
+ ## Backtest
26
+
27
+ ```bash
28
+ # real historical data (fetched via the platform's cached proxy)
29
+ speculo backtest strategy.py --symbol AAPL --timeframe 1d --capital 100000
30
+
31
+ # your own OHLCV CSV
32
+ speculo backtest strategy.py --csv mydata.csv
33
+
34
+ # synthetic demo series
35
+ speculo backtest strategy.py --bars 500
36
+ ```
37
+
38
+ Add `--no-sync` to keep a run private (compute locally, don't upload).
39
+
40
+ ## Strategy contract
41
+
42
+ A strategy defines `on_bar(ctx)` and optionally `initialize(ctx)` — identical to the
43
+ web editor:
44
+
45
+ ```python
46
+ def initialize(ctx):
47
+ ctx.state["fast"], ctx.state["slow"] = 5, 20
48
+
49
+ def on_bar(ctx):
50
+ c = ctx.closes()
51
+ if len(c) < ctx.state["slow"]:
52
+ return
53
+ fast = sum(c[-ctx.state["fast"]:]) / ctx.state["fast"]
54
+ slow = sum(c[-ctx.state["slow"]:]) / ctx.state["slow"]
55
+ if fast > slow and ctx.position == 0:
56
+ ctx.broker.place_order(symbol=ctx.symbol, quantity=10, side="BUY", order_type="MARKET")
57
+ elif fast < slow and ctx.position > 0:
58
+ ctx.broker.place_order(symbol=ctx.symbol, quantity=10, side="SELL", order_type="MARKET")
59
+ ```
60
+
61
+ The backtest engine itself is fetched from the platform at runtime, so the CLI always
62
+ runs the exact same engine as the web app.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "speculo"
7
+ version = "0.1.0"
8
+ description = "Speculo Studio CLI — backtest your trading strategies locally and sync results."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [{ name = "Speculo" }]
12
+ license = { text = "Proprietary" }
13
+ dependencies = [] # stdlib-only: the portable engine is fetched from the server at runtime.
14
+
15
+ [project.scripts]
16
+ speculo = "speculo.cli:main"
17
+
18
+ [project.urls]
19
+ Homepage = "https://speculotrading.com"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["."]
23
+ include = ["speculo*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Speculo Studio CLI — run backtests on your own machine and sync results.
2
+
3
+ The heavy lifting (the backtest engine) is the same portable ``speculo_engine`` that
4
+ powers the web app; the CLI fetches it from the server at runtime so it never drifts.
5
+ """
6
+ __version__ = "0.1.0"
@@ -0,0 +1,127 @@
1
+ """`speculo` CLI entrypoint.
2
+
3
+ Commands:
4
+ speculo login --token <tok> [--api URL] save credentials
5
+ speculo backtest STRATEGY.py [--symbol AAPL --timeframe 1d | --csv data.csv
6
+ | --bars 250] [--capital 1000000] [--no-sync]
7
+ speculo whoami show configured API / token prefix
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+
14
+ from . import __version__, config
15
+ from .client import ApiError, Client
16
+ from .engine_loader import load_engine
17
+
18
+
19
+ def _client() -> Client:
20
+ cfg = config.load()
21
+ if not cfg.get("token"):
22
+ print("Not logged in. Run: speculo login --token <your-token>", file=sys.stderr)
23
+ raise SystemExit(2)
24
+ return Client(cfg["api"], cfg["token"])
25
+
26
+
27
+ def cmd_login(args) -> int:
28
+ config.save(token=args.token, api=args.api)
29
+ print(f"Saved credentials for {args.api or config.DEFAULT_API}.")
30
+ return 0
31
+
32
+
33
+ def cmd_whoami(args) -> int:
34
+ cfg = config.load()
35
+ tok = cfg.get("token") or ""
36
+ print(f"API: {cfg['api']}")
37
+ print(f"Token: {tok[:10] + '…' if tok else '(none)'}")
38
+ return 0
39
+
40
+
41
+ def cmd_backtest(args) -> int:
42
+ client = _client()
43
+ source = open(args.strategy, encoding="utf-8").read()
44
+ print("Fetching engine…")
45
+ engine = load_engine(client)
46
+
47
+ # Resolve bars: local CSV → real market data → synthetic demo.
48
+ if args.csv:
49
+ bars = engine.load_csv(open(args.csv, encoding="utf-8").read())
50
+ if not bars:
51
+ print("No valid OHLCV rows in the CSV.", file=sys.stderr)
52
+ return 1
53
+ elif args.symbol:
54
+ print(f"Fetching {args.symbol} ({args.timeframe})…")
55
+ md = client.market_data(args.symbol, args.timeframe)
56
+ bars = engine.load_csv(md["csv_text"])
57
+ print(f" {md['rows']} bars from {md['source']}")
58
+ else:
59
+ bars = engine.synthetic(args.bars)
60
+
61
+ print("Running backtest locally…")
62
+ try:
63
+ result = engine.run_source(
64
+ source, bars, symbol=(args.symbol or "DEMO"),
65
+ starting_capital=args.capital,
66
+ )
67
+ except Exception as e: # noqa: BLE001 — surface engine/strategy errors plainly
68
+ print(f"Backtest failed: {e}", file=sys.stderr)
69
+ return 1
70
+
71
+ m = result["metrics"]
72
+ print("\n-- Results --")
73
+ print(f" Trades: {m['total_trades']}")
74
+ print(f" Win rate: {m['win_rate']}%")
75
+ print(f" Total return: {m['total_return_pct']}%")
76
+ print(f" Max drawdown: {m['max_drawdown_pct']}%")
77
+ print(f" Sharpe: {m['sharpe']}")
78
+ print(f" Final equity: {result['final_equity']}")
79
+
80
+ if not args.no_sync:
81
+ try:
82
+ saved = client.submit_backtest({
83
+ "name": args.name, "source": source, "symbol": (args.symbol or "DEMO"),
84
+ "timeframe": args.timeframe, "starting_capital": args.capital,
85
+ "metrics": result["metrics"], "equity_curve": result["equity_curve"],
86
+ "trades": result["trades"],
87
+ })
88
+ print(f"\nSynced to your Studio history (id {saved['id']}).")
89
+ except ApiError as e:
90
+ print(f"\nRan locally but could not sync: {e}", file=sys.stderr)
91
+ return 0
92
+
93
+
94
+ def main(argv=None) -> int:
95
+ p = argparse.ArgumentParser(prog="speculo", description="Speculo Studio CLI")
96
+ p.add_argument("--version", action="version", version=f"speculo {__version__}")
97
+ sub = p.add_subparsers(dest="cmd", required=True)
98
+
99
+ pl = sub.add_parser("login", help="save your CLI token")
100
+ pl.add_argument("--token", required=True)
101
+ pl.add_argument("--api", default=None, help="control-plane base URL")
102
+ pl.set_defaults(func=cmd_login)
103
+
104
+ pw = sub.add_parser("whoami", help="show configured credentials")
105
+ pw.set_defaults(func=cmd_whoami)
106
+
107
+ pb = sub.add_parser("backtest", help="run a strategy backtest locally")
108
+ pb.add_argument("strategy", help="path to a Python strategy (on_bar/initialize)")
109
+ pb.add_argument("--symbol", default=None, help="fetch real data for this symbol")
110
+ pb.add_argument("--timeframe", default="1d", choices=["1d", "1w", "1mo"])
111
+ pb.add_argument("--csv", default=None, help="use a local OHLCV CSV instead of fetching")
112
+ pb.add_argument("--bars", type=int, default=250, help="synthetic demo bars (no symbol/csv)")
113
+ pb.add_argument("--capital", type=float, default=1_000_000.0)
114
+ pb.add_argument("--name", default=None, help="label for this run in your history")
115
+ pb.add_argument("--no-sync", action="store_true", help="don't upload results")
116
+ pb.set_defaults(func=cmd_backtest)
117
+
118
+ args = p.parse_args(argv)
119
+ try:
120
+ return args.func(args)
121
+ except ApiError as e:
122
+ print(f"Error: {e}", file=sys.stderr)
123
+ return 1
124
+
125
+
126
+ if __name__ == "__main__":
127
+ raise SystemExit(main())
@@ -0,0 +1,49 @@
1
+ """Tiny stdlib HTTP client for the Speculo control plane (CLI token auth)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import urllib.error
6
+ import urllib.request
7
+
8
+
9
+ class ApiError(RuntimeError):
10
+ pass
11
+
12
+
13
+ class Client:
14
+ def __init__(self, api: str, token: str):
15
+ self.api = api.rstrip("/")
16
+ self.token = token
17
+
18
+ def _req(self, method: str, path: str, body: dict | None = None) -> dict:
19
+ url = f"{self.api}{path}"
20
+ data = json.dumps(body).encode() if body is not None else None
21
+ req = urllib.request.Request(url, data=data, method=method)
22
+ req.add_header("X-Speculo-Token", self.token)
23
+ req.add_header("Accept", "application/json")
24
+ if data is not None:
25
+ req.add_header("Content-Type", "application/json")
26
+ try:
27
+ with urllib.request.urlopen(req, timeout=30) as resp:
28
+ raw = resp.read().decode()
29
+ return json.loads(raw) if raw else {}
30
+ except urllib.error.HTTPError as e:
31
+ detail = e.read().decode(errors="replace")
32
+ try:
33
+ detail = json.loads(detail).get("detail", detail)
34
+ except ValueError:
35
+ pass
36
+ raise ApiError(f"{e.code} {detail}") from e
37
+ except urllib.error.URLError as e:
38
+ raise ApiError(f"Could not reach {self.api}: {e.reason}") from e
39
+
40
+ def engine_bundle(self) -> dict:
41
+ return self._req("GET", "/studio/engine-bundle")
42
+
43
+ def market_data(self, symbol: str, timeframe: str) -> dict:
44
+ from urllib.parse import urlencode
45
+ qs = urlencode({"symbol": symbol, "timeframe": timeframe})
46
+ return self._req("GET", f"/studio/market-data?{qs}")
47
+
48
+ def submit_backtest(self, payload: dict) -> dict:
49
+ return self._req("POST", "/studio/backtests/cli", payload)
@@ -0,0 +1,45 @@
1
+ """Local CLI config — token + API base, stored under ~/.speculo/config.json."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import pathlib
7
+
8
+ DEFAULT_API = "https://speculotrading.com/cp"
9
+ _DIR = pathlib.Path(os.path.expanduser("~")) / ".speculo"
10
+ _FILE = _DIR / "config.json"
11
+
12
+
13
+ def load() -> dict:
14
+ # Environment overrides the file so CI can run without `speculo login`.
15
+ cfg: dict = {}
16
+ if _FILE.exists():
17
+ try:
18
+ cfg = json.loads(_FILE.read_text(encoding="utf-8"))
19
+ except (ValueError, OSError):
20
+ cfg = {}
21
+ if os.environ.get("SPECULO_TOKEN"):
22
+ cfg["token"] = os.environ["SPECULO_TOKEN"]
23
+ if os.environ.get("SPECULO_API"):
24
+ cfg["api"] = os.environ["SPECULO_API"]
25
+ cfg.setdefault("api", DEFAULT_API)
26
+ return cfg
27
+
28
+
29
+ def save(token: str | None = None, api: str | None = None) -> None:
30
+ cfg = {}
31
+ if _FILE.exists():
32
+ try:
33
+ cfg = json.loads(_FILE.read_text(encoding="utf-8"))
34
+ except (ValueError, OSError):
35
+ cfg = {}
36
+ if token is not None:
37
+ cfg["token"] = token
38
+ if api is not None:
39
+ cfg["api"] = api
40
+ _DIR.mkdir(parents=True, exist_ok=True)
41
+ _FILE.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
42
+ try: # best-effort: keep the token file private
43
+ os.chmod(_FILE, 0o600)
44
+ except OSError:
45
+ pass
@@ -0,0 +1,32 @@
1
+ """Fetch the portable engine bundle from the server and import it locally.
2
+
3
+ Keeps the CLI in lock-step with the web app's engine (single source of truth) — the
4
+ CLI ships no engine code of its own. The bundle is cached under ~/.speculo/engine/<ver>.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import importlib
9
+ import os
10
+ import pathlib
11
+ import sys
12
+
13
+ _CACHE = pathlib.Path(os.path.expanduser("~")) / ".speculo" / "engine"
14
+
15
+
16
+ def load_engine(client):
17
+ """Return the imported ``speculo_engine`` module, fetching/caching as needed."""
18
+ bundle = client.engine_bundle()
19
+ version = bundle.get("version", "dev")
20
+ dest = _CACHE / version
21
+ pkg = dest / "speculo_engine"
22
+ if not pkg.exists():
23
+ pkg.mkdir(parents=True, exist_ok=True)
24
+ for name, src in bundle["files"].items():
25
+ (pkg / name).write_text(src, encoding="utf-8")
26
+
27
+ if str(dest) not in sys.path:
28
+ sys.path.insert(0, str(dest))
29
+ # Drop any previously-imported version so a server upgrade is picked up.
30
+ for mod in [m for m in list(sys.modules) if m.startswith("speculo_engine")]:
31
+ del sys.modules[mod]
32
+ return importlib.import_module("speculo_engine")
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: speculo
3
+ Version: 0.1.0
4
+ Summary: Speculo Studio CLI — backtest your trading strategies locally and sync results.
5
+ Author: Speculo
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://speculotrading.com
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Speculo CLI
12
+
13
+ Run Speculo Studio backtests on your own machine — your hardware, your data, results
14
+ synced to your Studio history.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install speculo
20
+ ```
21
+
22
+ ## Authenticate
23
+
24
+ Create a CLI token in Studio → Settings → CLI tokens, then:
25
+
26
+ ```bash
27
+ speculo login --token spk_xxxxxxxxxxxx
28
+ # self-hosted / staging:
29
+ speculo login --token spk_xxx --api https://api.staging.speculotrading.com
30
+ ```
31
+
32
+ Credentials are stored in `~/.speculo/config.json` (mode 600). You can also set
33
+ `SPECULO_TOKEN` / `SPECULO_API` in the environment.
34
+
35
+ ## Backtest
36
+
37
+ ```bash
38
+ # real historical data (fetched via the platform's cached proxy)
39
+ speculo backtest strategy.py --symbol AAPL --timeframe 1d --capital 100000
40
+
41
+ # your own OHLCV CSV
42
+ speculo backtest strategy.py --csv mydata.csv
43
+
44
+ # synthetic demo series
45
+ speculo backtest strategy.py --bars 500
46
+ ```
47
+
48
+ Add `--no-sync` to keep a run private (compute locally, don't upload).
49
+
50
+ ## Strategy contract
51
+
52
+ A strategy defines `on_bar(ctx)` and optionally `initialize(ctx)` — identical to the
53
+ web editor:
54
+
55
+ ```python
56
+ def initialize(ctx):
57
+ ctx.state["fast"], ctx.state["slow"] = 5, 20
58
+
59
+ def on_bar(ctx):
60
+ c = ctx.closes()
61
+ if len(c) < ctx.state["slow"]:
62
+ return
63
+ fast = sum(c[-ctx.state["fast"]:]) / ctx.state["fast"]
64
+ slow = sum(c[-ctx.state["slow"]:]) / ctx.state["slow"]
65
+ if fast > slow and ctx.position == 0:
66
+ ctx.broker.place_order(symbol=ctx.symbol, quantity=10, side="BUY", order_type="MARKET")
67
+ elif fast < slow and ctx.position > 0:
68
+ ctx.broker.place_order(symbol=ctx.symbol, quantity=10, side="SELL", order_type="MARKET")
69
+ ```
70
+
71
+ The backtest engine itself is fetched from the platform at runtime, so the CLI always
72
+ runs the exact same engine as the web app.
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ speculo/__init__.py
4
+ speculo/cli.py
5
+ speculo/client.py
6
+ speculo/config.py
7
+ speculo/engine_loader.py
8
+ speculo.egg-info/PKG-INFO
9
+ speculo.egg-info/SOURCES.txt
10
+ speculo.egg-info/dependency_links.txt
11
+ speculo.egg-info/entry_points.txt
12
+ speculo.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ speculo = speculo.cli:main
@@ -0,0 +1 @@
1
+ speculo