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 +72 -0
- speculo-0.1.0/README.md +62 -0
- speculo-0.1.0/pyproject.toml +23 -0
- speculo-0.1.0/setup.cfg +4 -0
- speculo-0.1.0/speculo/__init__.py +6 -0
- speculo-0.1.0/speculo/cli.py +127 -0
- speculo-0.1.0/speculo/client.py +49 -0
- speculo-0.1.0/speculo/config.py +45 -0
- speculo-0.1.0/speculo/engine_loader.py +32 -0
- speculo-0.1.0/speculo.egg-info/PKG-INFO +72 -0
- speculo-0.1.0/speculo.egg-info/SOURCES.txt +12 -0
- speculo-0.1.0/speculo.egg-info/dependency_links.txt +1 -0
- speculo-0.1.0/speculo.egg-info/entry_points.txt +2 -0
- speculo-0.1.0/speculo.egg-info/top_level.txt +1 -0
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.
|
speculo-0.1.0/README.md
ADDED
|
@@ -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*"]
|
speculo-0.1.0/setup.cfg
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
speculo
|