tasilab 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.
tasilab-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: tasilab
3
+ Version: 0.1.0
4
+ Summary: Tasi Lab is the first API-native paper trading sandbox for the Saudi Stock Exchange, empowering developers to build, test, and deploy AI-driven trading strategies with zero financial risk.
5
+ Author-email: Jafar Albinmousa <binmousa@gmail.com>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://tasilab.com
8
+ Project-URL: Documentation, https://api.tasilab.com/docs
9
+ Project-URL: Issues, https://github.com/jafaralbinmousa/tasilab/issues
10
+ Project-URL: Source, https://github.com/jafaralbinmousa/tasilab
11
+ Keywords: tasi,tadawul,saudi,stock,exchange,paper-trading,backtesting,trading,sdk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Office/Business :: Financial :: Investment
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: httpx>=0.25.0
26
+
27
+ # Tasi Lab Python SDK
28
+
29
+ Tasi Lab is the first API-native paper trading sandbox for the Saudi Stock Exchange, empowering developers to build, test, and deploy AI-driven trading strategies with zero financial risk.
30
+
31
+ This package is the official Python client. Use it to pull historical TASI data, log backtests as experiments, and view results in the Tasi Lab dashboard.
32
+
33
+ - Dashboard: <https://tasilab.com>
34
+ - API docs: <https://api.tasilab.com/docs>
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install tasilab
40
+ ```
41
+
42
+ ## Quick start
43
+
44
+ ```python
45
+ from tasilab import TasiLab
46
+
47
+ tasi = TasiLab(api_key="YOUR_API_KEY")
48
+
49
+ # 1. Pull historical bars (Tasi Lab caches them for you)
50
+ hist = tasi.get_historical("1120", "2024-01-01", "2025-12-31")
51
+
52
+ # 2. Run your strategy locally → list of trade dicts
53
+ trades = my_strategy(hist["bars"])
54
+
55
+ # 3. Log the run as a Tasi Lab experiment (auto-seals on exit)
56
+ with tasi.create_experiment(
57
+ name="MACD 12/26/9 on Al Rajhi",
58
+ symbol="1120",
59
+ start_date="2024-01-01",
60
+ end_date="2025-12-31",
61
+ parameters={"fast": 12, "slow": 26, "signal": 9},
62
+ ) as exp:
63
+ exp.log_trades(trades)
64
+ exp.log_metrics({"total_return_sar": 439.64, "win_rate": 0.412})
65
+ ```
66
+
67
+ The `with` block automatically marks the experiment `completed` on clean exit and `failed` if an exception propagates, so experiments never get stuck in `running`.
68
+
69
+ ## Paper trading
70
+
71
+ The SDK also supports live paper-trading against the simulated exchange:
72
+
73
+ ```python
74
+ order = tasi.buy("2222", quantity=100)
75
+ portfolio = tasi.portfolio()
76
+ ```
77
+
78
+ See <https://tasilab.com> for the full surface.
79
+
80
+ ## License
81
+
82
+ Proprietary — see <https://tasilab.com> for terms.
@@ -0,0 +1,56 @@
1
+ # Tasi Lab Python SDK
2
+
3
+ Tasi Lab is the first API-native paper trading sandbox for the Saudi Stock Exchange, empowering developers to build, test, and deploy AI-driven trading strategies with zero financial risk.
4
+
5
+ This package is the official Python client. Use it to pull historical TASI data, log backtests as experiments, and view results in the Tasi Lab dashboard.
6
+
7
+ - Dashboard: <https://tasilab.com>
8
+ - API docs: <https://api.tasilab.com/docs>
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install tasilab
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ from tasilab import TasiLab
20
+
21
+ tasi = TasiLab(api_key="YOUR_API_KEY")
22
+
23
+ # 1. Pull historical bars (Tasi Lab caches them for you)
24
+ hist = tasi.get_historical("1120", "2024-01-01", "2025-12-31")
25
+
26
+ # 2. Run your strategy locally → list of trade dicts
27
+ trades = my_strategy(hist["bars"])
28
+
29
+ # 3. Log the run as a Tasi Lab experiment (auto-seals on exit)
30
+ with tasi.create_experiment(
31
+ name="MACD 12/26/9 on Al Rajhi",
32
+ symbol="1120",
33
+ start_date="2024-01-01",
34
+ end_date="2025-12-31",
35
+ parameters={"fast": 12, "slow": 26, "signal": 9},
36
+ ) as exp:
37
+ exp.log_trades(trades)
38
+ exp.log_metrics({"total_return_sar": 439.64, "win_rate": 0.412})
39
+ ```
40
+
41
+ The `with` block automatically marks the experiment `completed` on clean exit and `failed` if an exception propagates, so experiments never get stuck in `running`.
42
+
43
+ ## Paper trading
44
+
45
+ The SDK also supports live paper-trading against the simulated exchange:
46
+
47
+ ```python
48
+ order = tasi.buy("2222", quantity=100)
49
+ portfolio = tasi.portfolio()
50
+ ```
51
+
52
+ See <https://tasilab.com> for the full surface.
53
+
54
+ ## License
55
+
56
+ Proprietary — see <https://tasilab.com> for terms.
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tasilab"
7
+ version = "0.1.0"
8
+ description = "Tasi Lab is the first API-native paper trading sandbox for the Saudi Stock Exchange, empowering developers to build, test, and deploy AI-driven trading strategies with zero financial risk."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Proprietary" }
12
+ authors = [{ name = "Jafar Albinmousa", email = "binmousa@gmail.com" }]
13
+ keywords = ["tasi", "tadawul", "saudi", "stock", "exchange", "paper-trading", "backtesting", "trading", "sdk"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Office/Business :: Financial :: Investment",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ ]
27
+ dependencies = ["httpx>=0.25.0"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://tasilab.com"
31
+ Documentation = "https://api.tasilab.com/docs"
32
+ Issues = "https://github.com/jafaralbinmousa/tasilab/issues"
33
+ Source = "https://github.com/jafaralbinmousa/tasilab"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["."]
37
+ include = ["tasilab*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Tasi Lab Python SDK — paper trading sandbox for the Saudi Exchange."""
2
+
3
+ from tasilab.client import Experiment, TasiLab
4
+
5
+ __all__ = ["TasiLab", "Experiment"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,662 @@
1
+ """TasiLab SDK client — synchronous Python wrapper for the Tasi Lab API.
2
+
3
+ Usage:
4
+ from tasilab import TasiLab
5
+
6
+ tasi = TasiLab(api_key="your-api-key")
7
+ quote = tasi.get_quote("2222")
8
+ order = tasi.buy("2222", quantity=100)
9
+ portfolio = tasi.portfolio()
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ import httpx
17
+
18
+
19
+ DEFAULT_BASE_URL = "https://api.tasilab.com"
20
+
21
+
22
+ class TasiLabError(Exception):
23
+ """Raised when the Tasi Lab API returns an error."""
24
+
25
+ def __init__(self, status_code: int, error: str, message: str) -> None:
26
+ self.status_code = status_code
27
+ self.error = error
28
+ self.message = message
29
+ super().__init__(f"[{status_code}] {error}: {message}")
30
+
31
+
32
+ class TasiLab:
33
+ """Synchronous client for the Tasi Lab paper trading API.
34
+
35
+ Args:
36
+ api_key: Your API key from POST /v1/auth/register.
37
+ base_url: API base URL. Defaults to https://api.tasilab.com.
38
+ timeout: Request timeout in seconds. Defaults to 30.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ api_key: str,
44
+ base_url: str = DEFAULT_BASE_URL,
45
+ timeout: float = 30.0,
46
+ ) -> None:
47
+ self._client = httpx.Client(
48
+ base_url=base_url,
49
+ headers={"X-API-Key": api_key},
50
+ timeout=timeout,
51
+ )
52
+
53
+ def close(self) -> None:
54
+ """Close the underlying HTTP client."""
55
+ self._client.close()
56
+
57
+ def __enter__(self) -> TasiLab:
58
+ return self
59
+
60
+ def __exit__(self, *args: Any) -> None:
61
+ self.close()
62
+
63
+ def _request(self, method: str, path: str, **kwargs: Any) -> dict | list:
64
+ response = self._client.request(method, path, **kwargs)
65
+ if response.status_code >= 400:
66
+ body = response.json()
67
+ detail = body.get("detail", body)
68
+ if isinstance(detail, dict):
69
+ raise TasiLabError(
70
+ status_code=response.status_code,
71
+ error=detail.get("error", "UNKNOWN"),
72
+ message=detail.get("message", str(detail)),
73
+ )
74
+ raise TasiLabError(
75
+ status_code=response.status_code,
76
+ error="UNKNOWN",
77
+ message=str(detail),
78
+ )
79
+ return response.json()
80
+
81
+ # ── Auth ──────────────────────────────────────────────
82
+
83
+ @staticmethod
84
+ def register(
85
+ email: str,
86
+ password: str,
87
+ base_url: str = DEFAULT_BASE_URL,
88
+ ) -> dict:
89
+ """Register a new account and get an API key.
90
+
91
+ Returns:
92
+ {"id": "...", "email": "...", "api_key": "...", "created_at": "..."}
93
+ """
94
+ response = httpx.post(
95
+ f"{base_url}/v1/auth/register",
96
+ json={"email": email, "password": password},
97
+ )
98
+ if response.status_code >= 400:
99
+ body = response.json()
100
+ detail = body.get("detail", body)
101
+ if isinstance(detail, dict):
102
+ raise TasiLabError(
103
+ status_code=response.status_code,
104
+ error=detail.get("error", "UNKNOWN"),
105
+ message=detail.get("message", str(detail)),
106
+ )
107
+ raise TasiLabError(response.status_code, "UNKNOWN", str(detail))
108
+ return response.json()
109
+
110
+ # ── Market Data ───────────────────────────────────────
111
+
112
+ def get_quote(self, symbol: str) -> dict:
113
+ """Get a real-time quote for a TASI symbol.
114
+
115
+ Args:
116
+ symbol: TASI symbol (e.g. "2222" for Aramco).
117
+
118
+ Returns:
119
+ Quote dict with price, change, volume, bid, ask, etc.
120
+ """
121
+ return self._request("GET", f"/v1/market/quote/{symbol}")
122
+
123
+ def get_quotes(self, symbols: list[str]) -> list[dict]:
124
+ """Get quotes for multiple symbols.
125
+
126
+ Args:
127
+ symbols: List of TASI symbols.
128
+
129
+ Returns:
130
+ List of quote dicts.
131
+ """
132
+ return self._request(
133
+ "GET", "/v1/market/quotes", params={"symbols": ",".join(symbols)}
134
+ )
135
+
136
+ def market_status(self) -> dict:
137
+ """Get TASI market status — index value, advancing/declining, mood.
138
+
139
+ Returns:
140
+ MarketStatus dict.
141
+ """
142
+ return self._request("GET", "/v1/market/status")
143
+
144
+ # ── Orders ────────────────────────────────────────────
145
+
146
+ def place_order(
147
+ self,
148
+ symbol: str,
149
+ side: str,
150
+ order_type: str,
151
+ quantity: int,
152
+ limit_price: float | None = None,
153
+ idempotency_key: str | None = None,
154
+ ) -> dict:
155
+ """Place a market or limit order.
156
+
157
+ Args:
158
+ symbol: TASI symbol.
159
+ side: "BUY" or "SELL".
160
+ order_type: "MARKET" or "LIMIT".
161
+ quantity: Number of shares.
162
+ limit_price: Required for LIMIT orders.
163
+ idempotency_key: Optional UUID for idempotent submission.
164
+
165
+ Returns:
166
+ Order dict with status, filled_price, trades, etc.
167
+ """
168
+ body: dict[str, Any] = {
169
+ "symbol": symbol,
170
+ "side": side,
171
+ "order_type": order_type,
172
+ "quantity": quantity,
173
+ }
174
+ if limit_price is not None:
175
+ body["limit_price"] = limit_price
176
+ if idempotency_key is not None:
177
+ body["idempotency_key"] = idempotency_key
178
+ return self._request("POST", "/v1/orders", json=body)
179
+
180
+ def buy(
181
+ self,
182
+ symbol: str,
183
+ quantity: int,
184
+ limit_price: float | None = None,
185
+ ) -> dict:
186
+ """Shortcut: place a BUY order (market if no limit_price, limit otherwise).
187
+
188
+ Args:
189
+ symbol: TASI symbol.
190
+ quantity: Number of shares.
191
+ limit_price: If set, places a LIMIT order.
192
+
193
+ Returns:
194
+ Order dict.
195
+ """
196
+ order_type = "LIMIT" if limit_price is not None else "MARKET"
197
+ return self.place_order(symbol, "BUY", order_type, quantity, limit_price)
198
+
199
+ def sell(
200
+ self,
201
+ symbol: str,
202
+ quantity: int,
203
+ limit_price: float | None = None,
204
+ ) -> dict:
205
+ """Shortcut: place a SELL order.
206
+
207
+ Args:
208
+ symbol: TASI symbol.
209
+ quantity: Number of shares.
210
+ limit_price: If set, places a LIMIT order.
211
+
212
+ Returns:
213
+ Order dict.
214
+ """
215
+ order_type = "LIMIT" if limit_price is not None else "MARKET"
216
+ return self.place_order(symbol, "SELL", order_type, quantity, limit_price)
217
+
218
+ def get_orders(
219
+ self,
220
+ symbol: str | None = None,
221
+ status: str | None = None,
222
+ limit: int = 50,
223
+ ) -> list[dict]:
224
+ """List orders.
225
+
226
+ Args:
227
+ symbol: Filter by symbol.
228
+ status: Filter by status (PENDING, FILLED, CANCELLED, REJECTED).
229
+ limit: Max results (default 50, max 100).
230
+
231
+ Returns:
232
+ List of order dicts.
233
+ """
234
+ params: dict[str, Any] = {"limit": limit}
235
+ if symbol:
236
+ params["symbol"] = symbol
237
+ if status:
238
+ params["status"] = status
239
+ return self._request("GET", "/v1/orders", params=params)
240
+
241
+ def get_order(self, order_id: str) -> dict:
242
+ """Get a specific order by ID.
243
+
244
+ Args:
245
+ order_id: UUID of the order.
246
+
247
+ Returns:
248
+ Order dict.
249
+ """
250
+ return self._request("GET", f"/v1/orders/{order_id}")
251
+
252
+ # ── Portfolio ─────────────────────────────────────────
253
+
254
+ def portfolio(self) -> dict:
255
+ """Get portfolio summary with total return.
256
+
257
+ Returns:
258
+ Dict with portfolio, total_value, total_return, total_return_pct.
259
+ """
260
+ return self._request("GET", "/v1/portfolio")
261
+
262
+ def positions(self) -> list[dict]:
263
+ """Get all open positions with live values.
264
+
265
+ Returns:
266
+ List of position dicts.
267
+ """
268
+ return self._request("GET", "/v1/portfolio/positions")
269
+
270
+ def trades(
271
+ self,
272
+ symbol: str | None = None,
273
+ limit: int = 50,
274
+ ) -> list[dict]:
275
+ """Get trade history.
276
+
277
+ Args:
278
+ symbol: Filter by symbol.
279
+ limit: Max results.
280
+
281
+ Returns:
282
+ List of trade dicts.
283
+ """
284
+ params: dict[str, Any] = {"limit": limit}
285
+ if symbol:
286
+ params["symbol"] = symbol
287
+ return self._request("GET", "/v1/portfolio/trades", params=params)
288
+
289
+ def snapshots(self, limit: int = 30) -> list[dict]:
290
+ """Get daily portfolio value snapshots.
291
+
292
+ Args:
293
+ limit: Max results.
294
+
295
+ Returns:
296
+ List of snapshot dicts.
297
+ """
298
+ return self._request("GET", "/v1/portfolio/snapshots", params={"limit": limit})
299
+
300
+ # ── Analytics ─────────────────────────────────────────
301
+
302
+ def performance(self) -> dict:
303
+ """Get portfolio performance metrics.
304
+
305
+ Returns:
306
+ Dict with total_return, win_rate, trade stats, etc.
307
+ """
308
+ return self._request("GET", "/v1/analytics/performance")
309
+
310
+ def daily_returns(self, limit: int = 30) -> list[dict]:
311
+ """Get daily portfolio returns.
312
+
313
+ Args:
314
+ limit: Number of days.
315
+
316
+ Returns:
317
+ List of daily return dicts.
318
+ """
319
+ return self._request(
320
+ "GET", "/v1/analytics/daily-returns", params={"limit": limit}
321
+ )
322
+
323
+ # ── Historical Data ──────────────────────────────────
324
+
325
+ def get_historical(self, symbol: str, start: str, end: str) -> dict:
326
+ """Get historical OHLCV data for a TASI symbol.
327
+
328
+ Args:
329
+ symbol: TASI symbol (e.g. "2222").
330
+ start: Start date, YYYY-MM-DD.
331
+ end: End date, YYYY-MM-DD.
332
+
333
+ Returns:
334
+ Dict with symbol, start, end, count, and bars list.
335
+ """
336
+ return self._request(
337
+ "GET",
338
+ f"/v1/historical/{symbol}",
339
+ params={"start": start, "end": end},
340
+ )
341
+
342
+ # ── Experiments ──────────────────────────────────────
343
+
344
+ def create_experiment(
345
+ self,
346
+ name: str,
347
+ symbol: str,
348
+ start_date: str | None = None,
349
+ end_date: str | None = None,
350
+ description: str | None = None,
351
+ parameters: dict | None = None,
352
+ experiment_type: str = "technical_indicator",
353
+ ml_model_config: dict | None = None,
354
+ ml_dataset_config: dict | None = None,
355
+ ) -> "Experiment":
356
+ """Create a new backtest experiment.
357
+
358
+ Args:
359
+ name: Strategy label (e.g. "SMA Crossover").
360
+ symbol: TASI symbol (e.g. "2222") or "MULTI" for multi-symbol.
361
+ start_date: Backtest start date, YYYY-MM-DD.
362
+ end_date: Backtest end date, YYYY-MM-DD.
363
+ description: Free-text description.
364
+ parameters: Strategy parameters dict.
365
+ experiment_type: "technical_indicator" (default) or "machine_learning".
366
+ ml_model_config: ML model spec. Required when experiment_type is
367
+ "machine_learning". Must include "framework" and "model_type";
368
+ extra keys (hyperparameters, features, etc.) allowed.
369
+ ml_dataset_config: ML dataset split. Required when experiment_type is
370
+ "machine_learning". Shape:
371
+ {"train": {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"},
372
+ "test": {...}, "val": {...}} (val optional).
373
+
374
+ Returns:
375
+ Experiment helper object for fluent logging.
376
+ """
377
+ body: dict[str, Any] = {
378
+ "name": name,
379
+ "symbol": symbol,
380
+ "experiment_type": experiment_type,
381
+ }
382
+ if start_date is not None:
383
+ body["start_date"] = start_date
384
+ if end_date is not None:
385
+ body["end_date"] = end_date
386
+ if description is not None:
387
+ body["description"] = description
388
+ if parameters is not None:
389
+ body["parameters"] = parameters
390
+ if ml_model_config is not None:
391
+ body["ml_model_config"] = ml_model_config
392
+ if ml_dataset_config is not None:
393
+ body["ml_dataset_config"] = ml_dataset_config
394
+ data = self._request("POST", "/v1/experiments", json=body)
395
+ return Experiment(self, data)
396
+
397
+ def create_ml_experiment(
398
+ self,
399
+ name: str,
400
+ symbol: str,
401
+ framework: str,
402
+ model_type: str,
403
+ train: tuple[str, str],
404
+ test: tuple[str, str],
405
+ val: tuple[str, str] | None = None,
406
+ description: str | None = None,
407
+ parameters: dict | None = None,
408
+ **model_extra: Any,
409
+ ) -> "Experiment":
410
+ """Create an ML experiment — convenience wrapper over create_experiment.
411
+
412
+ Args:
413
+ name: Experiment label.
414
+ symbol: TASI symbol or "MULTI".
415
+ framework: e.g. "sklearn", "xgboost", "pytorch".
416
+ model_type: e.g. "RandomForestClassifier".
417
+ train: (start, end) dates for the training set, YYYY-MM-DD.
418
+ test: (start, end) dates for the test set.
419
+ val: Optional (start, end) dates for the validation set.
420
+ description, parameters: as in create_experiment.
421
+ **model_extra: arbitrary extra keys merged into ml_model_config
422
+ (e.g. hyperparameters={...}, features=[...]).
423
+ """
424
+ model_cfg: dict[str, Any] = {"framework": framework, "model_type": model_type}
425
+ model_cfg.update(model_extra)
426
+
427
+ dataset_cfg: dict[str, Any] = {
428
+ "train": {"start": train[0], "end": train[1]},
429
+ "test": {"start": test[0], "end": test[1]},
430
+ }
431
+ if val is not None:
432
+ dataset_cfg["val"] = {"start": val[0], "end": val[1]}
433
+
434
+ return self.create_experiment(
435
+ name=name,
436
+ symbol=symbol,
437
+ description=description,
438
+ parameters=parameters,
439
+ experiment_type="machine_learning",
440
+ ml_model_config=model_cfg,
441
+ ml_dataset_config=dataset_cfg,
442
+ )
443
+
444
+ def list_experiments(
445
+ self,
446
+ status: str | None = None,
447
+ symbol: str | None = None,
448
+ experiment_type: str | None = None,
449
+ limit: int = 20,
450
+ offset: int = 0,
451
+ ) -> dict:
452
+ """List your experiments.
453
+
454
+ Args:
455
+ status: Filter by status (running, completed, failed).
456
+ symbol: Filter by symbol.
457
+ limit: Max results per page.
458
+ offset: Pagination offset.
459
+
460
+ Returns:
461
+ Dict with experiments list, total, limit, offset.
462
+ """
463
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
464
+ if status:
465
+ params["status"] = status
466
+ if symbol:
467
+ params["symbol"] = symbol
468
+ if experiment_type:
469
+ params["experiment_type"] = experiment_type
470
+ return self._request("GET", "/v1/experiments", params=params)
471
+
472
+ def get_experiment(self, experiment_id: str) -> "Experiment":
473
+ """Get an experiment by ID.
474
+
475
+ Args:
476
+ experiment_id: UUID of the experiment.
477
+
478
+ Returns:
479
+ Experiment helper object.
480
+ """
481
+ data = self._request("GET", f"/v1/experiments/{experiment_id}")
482
+ return Experiment(self, data)
483
+
484
+ def compare_experiments(self, experiment_ids: list[str]) -> dict:
485
+ """Compare two or more experiments side-by-side.
486
+
487
+ Args:
488
+ experiment_ids: List of experiment UUIDs (2–5).
489
+
490
+ Returns:
491
+ Dict with experiments list containing metrics for comparison.
492
+ """
493
+ return self._request(
494
+ "GET",
495
+ "/v1/experiments/compare",
496
+ params={"ids": ",".join(experiment_ids)},
497
+ )
498
+
499
+
500
+ class Experiment:
501
+ """Helper class wrapping an experiment for fluent logging.
502
+
503
+ Usage (context manager — recommended):
504
+ with tasi.create_experiment(name="SMA Crossover", symbol="2222") as exp:
505
+ exp.log_params({"fast": 10, "slow": 50})
506
+ exp.log_trades([...])
507
+ exp.log_metrics({"win_rate": 0.62})
508
+ # auto-sealed as completed on clean exit, failed on exception
509
+
510
+ Usage (manual):
511
+ exp = tasi.create_experiment(name="SMA Crossover", symbol="2222")
512
+ exp.log_metrics({"win_rate": 0.62})
513
+ exp.end()
514
+ """
515
+
516
+ def __init__(self, client: TasiLab, data: dict) -> None:
517
+ self._client = client
518
+ self.data = data
519
+ self.id: str = data["id"]
520
+
521
+ def __enter__(self) -> Experiment:
522
+ return self
523
+
524
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
525
+ if exc_type is not None:
526
+ self.fail()
527
+ else:
528
+ self.end()
529
+
530
+ def log_params(self, params: dict) -> dict:
531
+ """Log or merge strategy parameters.
532
+
533
+ Args:
534
+ params: Key-value parameter dict.
535
+
536
+ Returns:
537
+ Updated experiment dict.
538
+ """
539
+ self.data = self._client._request(
540
+ "POST",
541
+ f"/v1/experiments/{self.id}/params",
542
+ json={"parameters": params},
543
+ )
544
+ return self.data
545
+
546
+ def log_trade(
547
+ self,
548
+ side: str,
549
+ symbol: str,
550
+ price: float,
551
+ quantity: int,
552
+ date: str,
553
+ pnl: float | None = None,
554
+ commission: float = 0,
555
+ metadata: dict | None = None,
556
+ ) -> list[dict]:
557
+ """Log a single trade.
558
+
559
+ Args:
560
+ side: "BUY" or "SELL".
561
+ symbol: TASI symbol.
562
+ price: Execution price.
563
+ quantity: Number of shares.
564
+ date: Trade date, YYYY-MM-DD.
565
+ pnl: Optional per-trade P&L.
566
+ commission: Trading commission (default 0).
567
+ metadata: Optional per-trade metadata dict.
568
+
569
+ Returns:
570
+ List containing the created trade dict.
571
+ """
572
+ trade: dict[str, Any] = {
573
+ "side": side,
574
+ "symbol": symbol,
575
+ "price": price,
576
+ "quantity": quantity,
577
+ "date": date,
578
+ "commission": commission,
579
+ }
580
+ if pnl is not None:
581
+ trade["pnl"] = pnl
582
+ if metadata:
583
+ trade["metadata"] = metadata
584
+ return self._client._request(
585
+ "POST",
586
+ f"/v1/experiments/{self.id}/trades",
587
+ json={"trades": [trade]},
588
+ )
589
+
590
+ def log_trades(self, trades: list[dict]) -> list[dict]:
591
+ """Log multiple trades in a batch.
592
+
593
+ Args:
594
+ trades: List of trade dicts (side, symbol, price, quantity, date, ...).
595
+
596
+ Returns:
597
+ List of created trade dicts.
598
+ """
599
+ return self._client._request(
600
+ "POST",
601
+ f"/v1/experiments/{self.id}/trades",
602
+ json={"trades": trades},
603
+ )
604
+
605
+ def log_metrics(self, metrics: dict) -> dict:
606
+ """Log or merge performance metrics.
607
+
608
+ Args:
609
+ metrics: Key-value metrics dict (e.g. {"win_rate": 0.62}).
610
+
611
+ Returns:
612
+ Updated experiment dict.
613
+ """
614
+ self.data = self._client._request(
615
+ "POST",
616
+ f"/v1/experiments/{self.id}/metrics",
617
+ json={"metrics": metrics},
618
+ )
619
+ return self.data
620
+
621
+ def end(self) -> dict:
622
+ """Mark experiment as completed.
623
+
624
+ Idempotent — returns current data if already sealed.
625
+ """
626
+ if self.data.get("status") in ("completed", "failed"):
627
+ return self.data
628
+ self.data = self._client._request(
629
+ "PATCH",
630
+ f"/v1/experiments/{self.id}",
631
+ json={"status": "completed"},
632
+ )
633
+ return self.data
634
+
635
+ def fail(self) -> dict:
636
+ """Mark experiment as failed.
637
+
638
+ Idempotent — returns current data if already sealed.
639
+ """
640
+ if self.data.get("status") in ("completed", "failed"):
641
+ return self.data
642
+ self.data = self._client._request(
643
+ "PATCH",
644
+ f"/v1/experiments/{self.id}",
645
+ json={"status": "failed"},
646
+ )
647
+ return self.data
648
+
649
+ def compare(self, other_id: str) -> dict:
650
+ """Compare this experiment with another.
651
+
652
+ Args:
653
+ other_id: UUID of the other experiment.
654
+
655
+ Returns:
656
+ Dict with both experiments' metrics for comparison.
657
+ """
658
+ return self._client.compare_experiments([self.id, other_id])
659
+
660
+ def delete(self) -> None:
661
+ """Delete this experiment and all its trades."""
662
+ self._client._request("DELETE", f"/v1/experiments/{self.id}")
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: tasilab
3
+ Version: 0.1.0
4
+ Summary: Tasi Lab is the first API-native paper trading sandbox for the Saudi Stock Exchange, empowering developers to build, test, and deploy AI-driven trading strategies with zero financial risk.
5
+ Author-email: Jafar Albinmousa <binmousa@gmail.com>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://tasilab.com
8
+ Project-URL: Documentation, https://api.tasilab.com/docs
9
+ Project-URL: Issues, https://github.com/jafaralbinmousa/tasilab/issues
10
+ Project-URL: Source, https://github.com/jafaralbinmousa/tasilab
11
+ Keywords: tasi,tadawul,saudi,stock,exchange,paper-trading,backtesting,trading,sdk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Office/Business :: Financial :: Investment
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: httpx>=0.25.0
26
+
27
+ # Tasi Lab Python SDK
28
+
29
+ Tasi Lab is the first API-native paper trading sandbox for the Saudi Stock Exchange, empowering developers to build, test, and deploy AI-driven trading strategies with zero financial risk.
30
+
31
+ This package is the official Python client. Use it to pull historical TASI data, log backtests as experiments, and view results in the Tasi Lab dashboard.
32
+
33
+ - Dashboard: <https://tasilab.com>
34
+ - API docs: <https://api.tasilab.com/docs>
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install tasilab
40
+ ```
41
+
42
+ ## Quick start
43
+
44
+ ```python
45
+ from tasilab import TasiLab
46
+
47
+ tasi = TasiLab(api_key="YOUR_API_KEY")
48
+
49
+ # 1. Pull historical bars (Tasi Lab caches them for you)
50
+ hist = tasi.get_historical("1120", "2024-01-01", "2025-12-31")
51
+
52
+ # 2. Run your strategy locally → list of trade dicts
53
+ trades = my_strategy(hist["bars"])
54
+
55
+ # 3. Log the run as a Tasi Lab experiment (auto-seals on exit)
56
+ with tasi.create_experiment(
57
+ name="MACD 12/26/9 on Al Rajhi",
58
+ symbol="1120",
59
+ start_date="2024-01-01",
60
+ end_date="2025-12-31",
61
+ parameters={"fast": 12, "slow": 26, "signal": 9},
62
+ ) as exp:
63
+ exp.log_trades(trades)
64
+ exp.log_metrics({"total_return_sar": 439.64, "win_rate": 0.412})
65
+ ```
66
+
67
+ The `with` block automatically marks the experiment `completed` on clean exit and `failed` if an exception propagates, so experiments never get stuck in `running`.
68
+
69
+ ## Paper trading
70
+
71
+ The SDK also supports live paper-trading against the simulated exchange:
72
+
73
+ ```python
74
+ order = tasi.buy("2222", quantity=100)
75
+ portfolio = tasi.portfolio()
76
+ ```
77
+
78
+ See <https://tasilab.com> for the full surface.
79
+
80
+ ## License
81
+
82
+ Proprietary — see <https://tasilab.com> for terms.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ tasilab/__init__.py
4
+ tasilab/client.py
5
+ tasilab.egg-info/PKG-INFO
6
+ tasilab.egg-info/SOURCES.txt
7
+ tasilab.egg-info/dependency_links.txt
8
+ tasilab.egg-info/requires.txt
9
+ tasilab.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ httpx>=0.25.0
@@ -0,0 +1 @@
1
+ tasilab