cobweb-py 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.
- cobweb_py-0.1.0/PKG-INFO +25 -0
- cobweb_py-0.1.0/README.md +62 -0
- cobweb_py-0.1.0/cobweb_py/__init__.py +51 -0
- cobweb_py-0.1.0/cobweb_py/client.py +356 -0
- cobweb_py-0.1.0/cobweb_py/easy.py +357 -0
- cobweb_py-0.1.0/cobweb_py/features.py +73 -0
- cobweb_py-0.1.0/cobweb_py/plots.py +1122 -0
- cobweb_py-0.1.0/cobweb_py/scoring.py +568 -0
- cobweb_py-0.1.0/cobweb_py/utils.py +402 -0
- cobweb_py-0.1.0/cobweb_py.egg-info/PKG-INFO +25 -0
- cobweb_py-0.1.0/cobweb_py.egg-info/SOURCES.txt +32 -0
- cobweb_py-0.1.0/cobweb_py.egg-info/dependency_links.txt +1 -0
- cobweb_py-0.1.0/cobweb_py.egg-info/requires.txt +5 -0
- cobweb_py-0.1.0/cobweb_py.egg-info/top_level.txt +1 -0
- cobweb_py-0.1.0/pyproject.toml +41 -0
- cobweb_py-0.1.0/setup.cfg +21 -0
- cobweb_py-0.1.0/tests/test_api_backtest.py +70 -0
- cobweb_py-0.1.0/tests/test_api_features.py +49 -0
- cobweb_py-0.1.0/tests/test_api_plots.py +159 -0
- cobweb_py-0.1.0/tests/test_auth.py +114 -0
- cobweb_py-0.1.0/tests/test_backtest.py +222 -0
- cobweb_py-0.1.0/tests/test_dataio.py +77 -0
- cobweb_py-0.1.0/tests/test_entitlements.py +141 -0
- cobweb_py-0.1.0/tests/test_equity.py +103 -0
- cobweb_py-0.1.0/tests/test_execution.py +209 -0
- cobweb_py-0.1.0/tests/test_features_core.py +223 -0
- cobweb_py-0.1.0/tests/test_plot_payloads.py +148 -0
- cobweb_py-0.1.0/tests/test_rate_limit.py +50 -0
- cobweb_py-0.1.0/tests/test_schemas.py +204 -0
- cobweb_py-0.1.0/tests/test_sdk_client.py +168 -0
- cobweb_py-0.1.0/tests/test_sdk_plots.py +267 -0
- cobweb_py-0.1.0/tests/test_sdk_scoring.py +216 -0
- cobweb_py-0.1.0/tests/test_sdk_utils.py +206 -0
cobweb_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cobweb-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the CobwebSim stock simulation and backtesting API. Compute 71 technical features, run backtests with realistic execution modeling, and generate 27 plot types.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/cobwebSim/StockSim-
|
|
7
|
+
Project-URL: Documentation, https://web-production-83f3e.up.railway.app/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/cobwebSim/StockSim-
|
|
9
|
+
Project-URL: Issues, https://github.com/cobwebSim/StockSim-/issues
|
|
10
|
+
Keywords: trading,backtesting,finance,stock,simulation,technical-analysis,quantitative-finance
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: requests>=2.31
|
|
23
|
+
Provides-Extra: viz
|
|
24
|
+
Requires-Dist: pandas>=2.0; extra == "viz"
|
|
25
|
+
Requires-Dist: plotly>=5.0; extra == "viz"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# stocksim_easy
|
|
2
|
+
|
|
3
|
+
Beginner-friendly Python library for your **Stock Simulator API**.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
From your project venv:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -U requests pandas plotly
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then (option A) copy this folder into your project and import it.
|
|
14
|
+
|
|
15
|
+
Or (option B) install editable from this folder:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Super simple usage (recommended)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from stocksim_easy.easy import quickstart
|
|
25
|
+
|
|
26
|
+
result = quickstart(
|
|
27
|
+
base_url="http://127.0.0.1:8000",
|
|
28
|
+
csv_path="stock_data_csv",
|
|
29
|
+
feature_ids=[1, 11, 36], # ret_1d, sma_20, rsi_14
|
|
30
|
+
# If weights omitted, uses your default formula:
|
|
31
|
+
# score = 0.3*rsi_14 + 0.3*sma_signal + 0.4*ret_1d
|
|
32
|
+
out_dir="out",
|
|
33
|
+
signals="long",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
print("Score preview:", result["scores_preview"])
|
|
37
|
+
print("Open these files in your browser:", result["plots"])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## More control
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from stocksim_easy import StockSim
|
|
44
|
+
from stocksim_easy.scoring import compute_scores
|
|
45
|
+
from stocksim_easy.plots import save_price_and_score_plot
|
|
46
|
+
|
|
47
|
+
sim = StockSim("http://127.0.0.1:8000")
|
|
48
|
+
|
|
49
|
+
# 1) get features
|
|
50
|
+
feats = sim.ohlcv_with_features("stock_data_csv", feature_ids=[1, 11, 36])
|
|
51
|
+
|
|
52
|
+
# 2) compute score with custom weights (still returns a plain list)
|
|
53
|
+
scores = compute_scores(feats, {"rsi_14": 0.3, "sma_signal": 0.3, "ret_1d": 0.4})
|
|
54
|
+
|
|
55
|
+
# 3) make a plot HTML file
|
|
56
|
+
save_price_and_score_plot(feats, scores, out_html="price_and_score.html")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- Users don’t need to know pandas/matplotlib.
|
|
62
|
+
- Plots are HTML files. Double-click to open them in a browser.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""cobweb-py - Python client for CobwebSim."""
|
|
2
|
+
|
|
3
|
+
from .client import CobwebSim, CobwebError, BacktestConfig
|
|
4
|
+
from .scoring import (
|
|
5
|
+
score, score_by_id, auto_score, auto_score_by_id,
|
|
6
|
+
list_features, list_plots,
|
|
7
|
+
show_features, show_plots, show_categories,
|
|
8
|
+
FEATURE_CATS, PLOT_CATS,
|
|
9
|
+
)
|
|
10
|
+
from .utils import (
|
|
11
|
+
save_table,
|
|
12
|
+
fix_timestamps,
|
|
13
|
+
load_csv,
|
|
14
|
+
align,
|
|
15
|
+
to_signals,
|
|
16
|
+
get_plot,
|
|
17
|
+
save_all_plots,
|
|
18
|
+
get_signal,
|
|
19
|
+
print_signal,
|
|
20
|
+
)
|
|
21
|
+
from .plots import payload_to_figure, payloads_to_figures
|
|
22
|
+
from .easy import Pipeline
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CobwebSim",
|
|
26
|
+
"CobwebError",
|
|
27
|
+
"BacktestConfig",
|
|
28
|
+
"score",
|
|
29
|
+
"score_by_id",
|
|
30
|
+
"auto_score",
|
|
31
|
+
"auto_score_by_id",
|
|
32
|
+
"list_features",
|
|
33
|
+
"list_plots",
|
|
34
|
+
"show_features",
|
|
35
|
+
"show_plots",
|
|
36
|
+
"show_categories",
|
|
37
|
+
"FEATURE_CATS",
|
|
38
|
+
"PLOT_CATS",
|
|
39
|
+
"save_table",
|
|
40
|
+
"fix_timestamps",
|
|
41
|
+
"load_csv",
|
|
42
|
+
"align",
|
|
43
|
+
"to_signals",
|
|
44
|
+
"get_plot",
|
|
45
|
+
"save_all_plots",
|
|
46
|
+
"get_signal",
|
|
47
|
+
"print_signal",
|
|
48
|
+
"payload_to_figure",
|
|
49
|
+
"payloads_to_figures",
|
|
50
|
+
"Pipeline",
|
|
51
|
+
]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import pandas as pd # optional
|
|
12
|
+
except Exception: # pragma: no cover
|
|
13
|
+
pd = None # type: ignore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CobwebError(RuntimeError):
|
|
17
|
+
"""Raised when the API request fails or input data is invalid."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_pathlike(x: Any) -> bool:
|
|
21
|
+
return isinstance(x, (str, Path))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_csv_simple(csv_path: Union[str, Path], max_rows: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
25
|
+
"""Read a CSV file and convert to the API's OHLCRow list."""
|
|
26
|
+
if pd is None:
|
|
27
|
+
raise CobwebError("pandas is required to read CSV. Install with: pip install pandas")
|
|
28
|
+
|
|
29
|
+
p = Path(csv_path)
|
|
30
|
+
if not p.exists():
|
|
31
|
+
raise CobwebError(f"CSV not found: {p}")
|
|
32
|
+
|
|
33
|
+
df = pd.read_csv(p)
|
|
34
|
+
if max_rows:
|
|
35
|
+
df = df.head(max_rows).copy()
|
|
36
|
+
|
|
37
|
+
df.columns = [str(c).strip() for c in df.columns]
|
|
38
|
+
lower_map = {c.lower(): c for c in df.columns}
|
|
39
|
+
|
|
40
|
+
def pick(*names: str) -> Optional[str]:
|
|
41
|
+
for n in names:
|
|
42
|
+
if n.lower() in lower_map:
|
|
43
|
+
return lower_map[n.lower()]
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
c_open = pick("open", "Open")
|
|
47
|
+
c_high = pick("high", "High")
|
|
48
|
+
c_low = pick("low", "Low")
|
|
49
|
+
c_close = pick("close", "Close")
|
|
50
|
+
c_vol = pick("volume", "Volume", "vol")
|
|
51
|
+
c_ts = pick("timestamp", "time", "date", "datetime", "Date", "Datetime")
|
|
52
|
+
|
|
53
|
+
missing = [n for n, c in [("open", c_open), ("high", c_high), ("low", c_low), ("close", c_close)] if c is None]
|
|
54
|
+
if missing:
|
|
55
|
+
raise CobwebError(f"CSV missing required columns: {missing}. Found: {list(df.columns)}")
|
|
56
|
+
|
|
57
|
+
for c in [c_open, c_high, c_low, c_close]:
|
|
58
|
+
df[c] = pd.to_numeric(df[c], errors="coerce")
|
|
59
|
+
if c_vol is not None:
|
|
60
|
+
df[c_vol] = pd.to_numeric(df[c_vol], errors="coerce")
|
|
61
|
+
|
|
62
|
+
df = df.dropna(subset=[c_open, c_high, c_low, c_close]).reset_index(drop=True)
|
|
63
|
+
|
|
64
|
+
rows: List[Dict[str, Any]] = []
|
|
65
|
+
for _, r in df.iterrows():
|
|
66
|
+
row: Dict[str, Any] = {
|
|
67
|
+
"open": float(r[c_open]),
|
|
68
|
+
"high": float(r[c_high]),
|
|
69
|
+
"low": float(r[c_low]),
|
|
70
|
+
"close": float(r[c_close]),
|
|
71
|
+
}
|
|
72
|
+
if c_vol is not None and r.get(c_vol) == r.get(c_vol):
|
|
73
|
+
row["volume"] = float(r[c_vol])
|
|
74
|
+
if c_ts is not None and r.get(c_ts) == r.get(c_ts):
|
|
75
|
+
row["timestamp"] = str(r[c_ts])
|
|
76
|
+
rows.append(row)
|
|
77
|
+
|
|
78
|
+
if len(rows) < 20:
|
|
79
|
+
raise CobwebError("Need at least 20 rows for meaningful indicators like SMA/RSI.")
|
|
80
|
+
|
|
81
|
+
return rows
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_OHLCV_FIELDS = {"open", "high", "low", "close", "volume"}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _normalize_ohlcv_keys(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
88
|
+
"""Lowercase any OHLCV keys that came back capitalised from the API (e.g. Open → open)."""
|
|
89
|
+
if not rows:
|
|
90
|
+
return rows
|
|
91
|
+
if not any(k for k in rows[0] if k.lower() in _OHLCV_FIELDS and k != k.lower()):
|
|
92
|
+
return rows # nothing to fix — fast path
|
|
93
|
+
return [
|
|
94
|
+
{(k.lower() if k.lower() in _OHLCV_FIELDS else k): v for k, v in row.items()}
|
|
95
|
+
for row in rows
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _to_rows(data: Any, *, max_rows: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
100
|
+
"""Accepts: CSV path, pandas DataFrame, list[dict] rows, or dict {'rows':[...]}"""
|
|
101
|
+
if _is_pathlike(data):
|
|
102
|
+
return _read_csv_simple(data, max_rows=max_rows)
|
|
103
|
+
|
|
104
|
+
if isinstance(data, dict) and "rows" in data and isinstance(data["rows"], list):
|
|
105
|
+
return _normalize_ohlcv_keys(list(data["rows"]))
|
|
106
|
+
|
|
107
|
+
if isinstance(data, list) and (len(data) == 0 or isinstance(data[0], dict)):
|
|
108
|
+
return _normalize_ohlcv_keys(data)
|
|
109
|
+
|
|
110
|
+
if pd is not None and hasattr(data, "__class__") and data.__class__.__name__ == "DataFrame":
|
|
111
|
+
df = data.copy()
|
|
112
|
+
if max_rows:
|
|
113
|
+
df = df.head(max_rows).copy()
|
|
114
|
+
|
|
115
|
+
df.columns = [str(c).strip() for c in df.columns]
|
|
116
|
+
lower_map = {c.lower(): c for c in df.columns}
|
|
117
|
+
|
|
118
|
+
def pick(*names: str) -> Optional[str]:
|
|
119
|
+
for n in names:
|
|
120
|
+
if n.lower() in lower_map:
|
|
121
|
+
return lower_map[n.lower()]
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
c_open = pick("open", "Open")
|
|
125
|
+
c_high = pick("high", "High")
|
|
126
|
+
c_low = pick("low", "Low")
|
|
127
|
+
c_close = pick("close", "Close")
|
|
128
|
+
c_vol = pick("volume", "Volume", "vol")
|
|
129
|
+
c_ts = pick("timestamp", "time", "date", "datetime", "Date", "Datetime")
|
|
130
|
+
|
|
131
|
+
missing = [n for n, c in [("open", c_open), ("high", c_high), ("low", c_low), ("close", c_close)] if c is None]
|
|
132
|
+
if missing:
|
|
133
|
+
raise CobwebError(f"DataFrame missing required columns: {missing}. Found: {list(df.columns)}")
|
|
134
|
+
|
|
135
|
+
for c in [c_open, c_high, c_low, c_close]:
|
|
136
|
+
df[c] = pd.to_numeric(df[c], errors="coerce")
|
|
137
|
+
if c_vol is not None:
|
|
138
|
+
df[c_vol] = pd.to_numeric(df[c_vol], errors="coerce")
|
|
139
|
+
|
|
140
|
+
df = df.dropna(subset=[c_open, c_high, c_low, c_close]).reset_index(drop=True)
|
|
141
|
+
|
|
142
|
+
rows: List[Dict[str, Any]] = []
|
|
143
|
+
for _, r in df.iterrows():
|
|
144
|
+
row: Dict[str, Any] = {
|
|
145
|
+
"open": float(r[c_open]),
|
|
146
|
+
"high": float(r[c_high]),
|
|
147
|
+
"low": float(r[c_low]),
|
|
148
|
+
"close": float(r[c_close]),
|
|
149
|
+
}
|
|
150
|
+
if c_vol is not None and r.get(c_vol) == r.get(c_vol):
|
|
151
|
+
row["volume"] = float(r[c_vol])
|
|
152
|
+
if c_ts is not None and r.get(c_ts) == r.get(c_ts):
|
|
153
|
+
row["timestamp"] = str(r[c_ts])
|
|
154
|
+
rows.append(row)
|
|
155
|
+
|
|
156
|
+
if len(rows) < 20:
|
|
157
|
+
raise CobwebError("Need at least 20 rows for meaningful indicators like SMA/RSI.")
|
|
158
|
+
return rows
|
|
159
|
+
|
|
160
|
+
raise CobwebError(
|
|
161
|
+
"Unsupported data input. Use a CSV path, a pandas DataFrame, "
|
|
162
|
+
"a list of OHLC row dicts, or a dict with {'rows': [...]}."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class BacktestConfig:
|
|
168
|
+
"""
|
|
169
|
+
Configuration for a backtest run.
|
|
170
|
+
|
|
171
|
+
Pass to sim.backtest(config=...) — all fields are optional, defaults match the API.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
cfg = BacktestConfig(
|
|
175
|
+
initial_cash=50_000,
|
|
176
|
+
exec_horizon="longterm",
|
|
177
|
+
fee_bps=0.5,
|
|
178
|
+
rebalance_mode="calendar",
|
|
179
|
+
rebalance_every_n_bars=5,
|
|
180
|
+
)
|
|
181
|
+
bt = sim.backtest(data, signals=signals, config=cfg)
|
|
182
|
+
"""
|
|
183
|
+
# --- Capital ---
|
|
184
|
+
initial_cash: float = 10_000.0
|
|
185
|
+
max_leverage: float = 1.0
|
|
186
|
+
allow_margin: bool = False
|
|
187
|
+
|
|
188
|
+
# --- Execution ---
|
|
189
|
+
exec_horizon: str = "swing" # "intraday" | "swing" | "longterm"
|
|
190
|
+
asset_type: str = "equities" # "equities" | "crypto"
|
|
191
|
+
max_participation: Optional[float] = None # max fraction of bar volume per trade
|
|
192
|
+
|
|
193
|
+
# --- Costs ---
|
|
194
|
+
fee_bps: float = 1.0
|
|
195
|
+
half_spread_bps: float = 2.0
|
|
196
|
+
base_slippage_bps: float = 1.0
|
|
197
|
+
impact_coeff: float = 1.0
|
|
198
|
+
|
|
199
|
+
# --- Rebalancing ---
|
|
200
|
+
rebalance_mode: str = "on_signal_change" # "on_signal_change" | "every_bar" | "calendar"
|
|
201
|
+
rebalance_every_n_bars: int = 0 # only used when rebalance_mode="calendar"
|
|
202
|
+
min_trade_units: float = 0.0
|
|
203
|
+
max_order_age_bars: int = 0
|
|
204
|
+
|
|
205
|
+
# --- Signal thresholds ---
|
|
206
|
+
buy_threshold: float = 0.10
|
|
207
|
+
sell_threshold: float = -0.10
|
|
208
|
+
|
|
209
|
+
# --- Analytics ---
|
|
210
|
+
risk_free_annual: float = 0.0
|
|
211
|
+
periods_per_year: int = 252
|
|
212
|
+
|
|
213
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
214
|
+
"""Convert to a plain dict for passing as config= to sim.backtest()."""
|
|
215
|
+
import dataclasses
|
|
216
|
+
return {k: v for k, v in dataclasses.asdict(self).items() if v is not None}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass
|
|
220
|
+
class APIResponse:
|
|
221
|
+
status_code: int
|
|
222
|
+
json: Any
|
|
223
|
+
elapsed_ms: int
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class CobwebSim:
|
|
227
|
+
"""Friendly wrapper around the CobwebSim API."""
|
|
228
|
+
|
|
229
|
+
def __init__(self, base_url: str, api_key: Optional[str] = None, timeout: int = 60):
|
|
230
|
+
self.base_url = base_url.rstrip("/")
|
|
231
|
+
self.api_key = api_key
|
|
232
|
+
self.timeout = timeout
|
|
233
|
+
self._session = requests.Session()
|
|
234
|
+
|
|
235
|
+
def _headers(self) -> Dict[str, str]:
|
|
236
|
+
h = {"Content-Type": "application/json"}
|
|
237
|
+
if self.api_key:
|
|
238
|
+
h["Authorization"] = f"Bearer {self.api_key}"
|
|
239
|
+
return h
|
|
240
|
+
|
|
241
|
+
def _call(self, path: str, payload: Optional[dict] = None, method: str = "POST") -> APIResponse:
|
|
242
|
+
url = f"{self.base_url}{path}"
|
|
243
|
+
t0 = time.time()
|
|
244
|
+
try:
|
|
245
|
+
r = self._session.request(
|
|
246
|
+
method.upper(),
|
|
247
|
+
url,
|
|
248
|
+
headers=self._headers(),
|
|
249
|
+
json=payload,
|
|
250
|
+
timeout=self.timeout,
|
|
251
|
+
)
|
|
252
|
+
except requests.RequestException as e:
|
|
253
|
+
raise CobwebError(f"Request failed: {e}") from e
|
|
254
|
+
elapsed_ms = int((time.time() - t0) * 1000)
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
data = r.json()
|
|
258
|
+
except Exception:
|
|
259
|
+
data = {"text": (r.text or "")[:4000]}
|
|
260
|
+
|
|
261
|
+
if r.status_code >= 400:
|
|
262
|
+
raise CobwebError(f"{method} {path} -> HTTP {r.status_code}: {data}")
|
|
263
|
+
|
|
264
|
+
return APIResponse(status_code=r.status_code, json=data, elapsed_ms=elapsed_ms)
|
|
265
|
+
|
|
266
|
+
def health(self) -> dict:
|
|
267
|
+
return self._call("/openapi.json", payload=None, method="GET").json
|
|
268
|
+
|
|
269
|
+
def _prepare(self, data: Any, *, max_rows: Optional[int] = None) -> dict:
|
|
270
|
+
return {"rows": _to_rows(data, max_rows=max_rows)}
|
|
271
|
+
|
|
272
|
+
def enrich(
|
|
273
|
+
self, data: Any, feature_ids: Optional[List[int]] = None, *, max_rows: Optional[int] = None
|
|
274
|
+
) -> dict:
|
|
275
|
+
"""Return OHLCV rows enriched with computed feature columns."""
|
|
276
|
+
payload = {"data": self._prepare(data, max_rows=max_rows), "feature_ids": feature_ids}
|
|
277
|
+
return self._call("/features", payload).json
|
|
278
|
+
|
|
279
|
+
def features(self, data: Any, feature_ids: Optional[List[int]] = None, *, max_rows: Optional[int] = None) -> dict:
|
|
280
|
+
"""Alias for enrich()."""
|
|
281
|
+
return self.enrich(data, feature_ids=feature_ids, max_rows=max_rows)
|
|
282
|
+
|
|
283
|
+
# ✅ FIX 1: backtest is a proper CobwebSim method (indented under the class)
|
|
284
|
+
# ✅ FIX 2: use `.json` (field) not `.json()` (method)
|
|
285
|
+
def backtest(
|
|
286
|
+
self,
|
|
287
|
+
data: Any,
|
|
288
|
+
*,
|
|
289
|
+
signals: Union[List[float], str],
|
|
290
|
+
compute_features: bool = True,
|
|
291
|
+
feature_ids: Optional[List[int]] = None,
|
|
292
|
+
plot_ids: Optional[List[int]] = None,
|
|
293
|
+
plot_params: Optional[Dict[str, Any]] = None,
|
|
294
|
+
benchmark: Optional[Any] = None,
|
|
295
|
+
config: Optional[Union[Dict[str, Any], "BacktestConfig"]] = None,
|
|
296
|
+
max_rows: Optional[int] = None,
|
|
297
|
+
) -> dict:
|
|
298
|
+
rows = _to_rows(data, max_rows=max_rows)
|
|
299
|
+
n = len(rows)
|
|
300
|
+
|
|
301
|
+
if isinstance(signals, str):
|
|
302
|
+
s = signals.strip().lower()
|
|
303
|
+
if s in ("long", "buy"):
|
|
304
|
+
sigs = [1.0] * n
|
|
305
|
+
elif s in ("short", "sell"):
|
|
306
|
+
sigs = [-1.0] * n
|
|
307
|
+
elif s in ("flat", "none", "hold"):
|
|
308
|
+
sigs = [0.0] * n
|
|
309
|
+
else:
|
|
310
|
+
raise CobwebError("signals string must be one of: long/short/flat")
|
|
311
|
+
elif isinstance(signals, list):
|
|
312
|
+
sigs = signals
|
|
313
|
+
if len(sigs) != n:
|
|
314
|
+
raise CobwebError(f"signals length must equal number of rows ({n}); got {len(sigs)}")
|
|
315
|
+
else:
|
|
316
|
+
raise CobwebError("signals must be a list[float] or one of: 'long'/'short'/'flat'")
|
|
317
|
+
|
|
318
|
+
config_dict = config.to_dict() if isinstance(config, BacktestConfig) else (config or {})
|
|
319
|
+
|
|
320
|
+
payload = {
|
|
321
|
+
"data": {"rows": rows},
|
|
322
|
+
"compute_features": compute_features,
|
|
323
|
+
"feature_ids": feature_ids,
|
|
324
|
+
"plot_ids": plot_ids,
|
|
325
|
+
"plot_params": plot_params,
|
|
326
|
+
"benchmark": self._prepare(benchmark, max_rows=max_rows) if benchmark is not None else None,
|
|
327
|
+
"signals": sigs,
|
|
328
|
+
"config": config_dict,
|
|
329
|
+
}
|
|
330
|
+
return self._call("/backtest", payload).json
|
|
331
|
+
|
|
332
|
+
# ✅ FIX 3: plots is its own method (not nested inside backtest)
|
|
333
|
+
def plots(
|
|
334
|
+
self,
|
|
335
|
+
data: Any,
|
|
336
|
+
backtest_result: dict,
|
|
337
|
+
*,
|
|
338
|
+
compute_features: bool = False,
|
|
339
|
+
plot_ids: Optional[List[int]] = None,
|
|
340
|
+
requested: Optional[List[str]] = None,
|
|
341
|
+
plot_params: Optional[Dict[str, Any]] = None,
|
|
342
|
+
benchmark: Optional[Any] = None,
|
|
343
|
+
feature_ids: Optional[List[int]] = None,
|
|
344
|
+
max_rows: Optional[int] = None,
|
|
345
|
+
) -> dict:
|
|
346
|
+
payload = {
|
|
347
|
+
"data": self._prepare(data, max_rows=max_rows),
|
|
348
|
+
"compute_features": compute_features,
|
|
349
|
+
"backtest_result": backtest_result,
|
|
350
|
+
"feature_ids": feature_ids,
|
|
351
|
+
"plot_ids": plot_ids,
|
|
352
|
+
"plot_params": plot_params,
|
|
353
|
+
"benchmark": self._prepare(benchmark, max_rows=max_rows) if benchmark is not None else None,
|
|
354
|
+
"requested": requested,
|
|
355
|
+
}
|
|
356
|
+
return self._call("/plots", payload).json
|