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.
Files changed (33) hide show
  1. cobweb_py-0.1.0/PKG-INFO +25 -0
  2. cobweb_py-0.1.0/README.md +62 -0
  3. cobweb_py-0.1.0/cobweb_py/__init__.py +51 -0
  4. cobweb_py-0.1.0/cobweb_py/client.py +356 -0
  5. cobweb_py-0.1.0/cobweb_py/easy.py +357 -0
  6. cobweb_py-0.1.0/cobweb_py/features.py +73 -0
  7. cobweb_py-0.1.0/cobweb_py/plots.py +1122 -0
  8. cobweb_py-0.1.0/cobweb_py/scoring.py +568 -0
  9. cobweb_py-0.1.0/cobweb_py/utils.py +402 -0
  10. cobweb_py-0.1.0/cobweb_py.egg-info/PKG-INFO +25 -0
  11. cobweb_py-0.1.0/cobweb_py.egg-info/SOURCES.txt +32 -0
  12. cobweb_py-0.1.0/cobweb_py.egg-info/dependency_links.txt +1 -0
  13. cobweb_py-0.1.0/cobweb_py.egg-info/requires.txt +5 -0
  14. cobweb_py-0.1.0/cobweb_py.egg-info/top_level.txt +1 -0
  15. cobweb_py-0.1.0/pyproject.toml +41 -0
  16. cobweb_py-0.1.0/setup.cfg +21 -0
  17. cobweb_py-0.1.0/tests/test_api_backtest.py +70 -0
  18. cobweb_py-0.1.0/tests/test_api_features.py +49 -0
  19. cobweb_py-0.1.0/tests/test_api_plots.py +159 -0
  20. cobweb_py-0.1.0/tests/test_auth.py +114 -0
  21. cobweb_py-0.1.0/tests/test_backtest.py +222 -0
  22. cobweb_py-0.1.0/tests/test_dataio.py +77 -0
  23. cobweb_py-0.1.0/tests/test_entitlements.py +141 -0
  24. cobweb_py-0.1.0/tests/test_equity.py +103 -0
  25. cobweb_py-0.1.0/tests/test_execution.py +209 -0
  26. cobweb_py-0.1.0/tests/test_features_core.py +223 -0
  27. cobweb_py-0.1.0/tests/test_plot_payloads.py +148 -0
  28. cobweb_py-0.1.0/tests/test_rate_limit.py +50 -0
  29. cobweb_py-0.1.0/tests/test_schemas.py +204 -0
  30. cobweb_py-0.1.0/tests/test_sdk_client.py +168 -0
  31. cobweb_py-0.1.0/tests/test_sdk_plots.py +267 -0
  32. cobweb_py-0.1.0/tests/test_sdk_scoring.py +216 -0
  33. cobweb_py-0.1.0/tests/test_sdk_utils.py +206 -0
@@ -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