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 +82 -0
- tasilab-0.1.0/README.md +56 -0
- tasilab-0.1.0/pyproject.toml +37 -0
- tasilab-0.1.0/setup.cfg +4 -0
- tasilab-0.1.0/tasilab/__init__.py +6 -0
- tasilab-0.1.0/tasilab/client.py +662 -0
- tasilab-0.1.0/tasilab.egg-info/PKG-INFO +82 -0
- tasilab-0.1.0/tasilab.egg-info/SOURCES.txt +9 -0
- tasilab-0.1.0/tasilab.egg-info/dependency_links.txt +1 -0
- tasilab-0.1.0/tasilab.egg-info/requires.txt +1 -0
- tasilab-0.1.0/tasilab.egg-info/top_level.txt +1 -0
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.
|
tasilab-0.1.0/README.md
ADDED
|
@@ -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*"]
|
tasilab-0.1.0/setup.cfg
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.25.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tasilab
|