tradingkit-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.
- tradingkit_py-0.1.0/LICENSE +21 -0
- tradingkit_py-0.1.0/PKG-INFO +303 -0
- tradingkit_py-0.1.0/README.md +270 -0
- tradingkit_py-0.1.0/pyproject.toml +74 -0
- tradingkit_py-0.1.0/setup.cfg +4 -0
- tradingkit_py-0.1.0/tests/test_aggregation.py +93 -0
- tradingkit_py-0.1.0/tests/test_backtest.py +115 -0
- tradingkit_py-0.1.0/tests/test_clickhouse_identifiers.py +76 -0
- tradingkit_py-0.1.0/tests/test_clickhouse_integration.py +130 -0
- tradingkit_py-0.1.0/tests/test_clickhouse_manager.py +188 -0
- tradingkit_py-0.1.0/tests/test_collector.py +236 -0
- tradingkit_py-0.1.0/tests/test_cpp_pool.py +220 -0
- tradingkit_py-0.1.0/tests/test_executors.py +109 -0
- tradingkit_py-0.1.0/tests/test_indicator.py +129 -0
- tradingkit_py-0.1.0/tests/test_params.py +85 -0
- tradingkit_py-0.1.0/tests/test_pipeline.py +93 -0
- tradingkit_py-0.1.0/tests/test_plugin_registry.py +26 -0
- tradingkit_py-0.1.0/tests/test_remote_executor.py +59 -0
- tradingkit_py-0.1.0/tests/test_resolver.py +191 -0
- tradingkit_py-0.1.0/tests/test_runner_server.py +154 -0
- tradingkit_py-0.1.0/tests/test_safe_pickle.py +91 -0
- tradingkit_py-0.1.0/tests/test_schema.py +129 -0
- tradingkit_py-0.1.0/tests/test_source.py +116 -0
- tradingkit_py-0.1.0/tests/test_strategy.py +136 -0
- tradingkit_py-0.1.0/tests/test_timeframe.py +58 -0
- tradingkit_py-0.1.0/tradingkit/__init__.py +147 -0
- tradingkit_py-0.1.0/tradingkit/_docker/compiler/Dockerfile +6 -0
- tradingkit_py-0.1.0/tradingkit/_docker/compiler/compile.sh +19 -0
- tradingkit_py-0.1.0/tradingkit/_docker/runner/Dockerfile +7 -0
- tradingkit_py-0.1.0/tradingkit/_docker/runner/runner_worker.py +143 -0
- tradingkit_py-0.1.0/tradingkit/_docker/runner/seccomp.json +44 -0
- tradingkit_py-0.1.0/tradingkit/aggregation.py +329 -0
- tradingkit_py-0.1.0/tradingkit/backtest/__init__.py +4 -0
- tradingkit_py-0.1.0/tradingkit/backtest/result.py +100 -0
- tradingkit_py-0.1.0/tradingkit/backtest/runner.py +133 -0
- tradingkit_py-0.1.0/tradingkit/collector.py +572 -0
- tradingkit_py-0.1.0/tradingkit/context.py +38 -0
- tradingkit_py-0.1.0/tradingkit/core/__init__.py +0 -0
- tradingkit_py-0.1.0/tradingkit/core/clickhouse/__init__.py +136 -0
- tradingkit_py-0.1.0/tradingkit/core/clickhouse/_candles.py +234 -0
- tradingkit_py-0.1.0/tradingkit/core/clickhouse/_connections.py +90 -0
- tradingkit_py-0.1.0/tradingkit/core/clickhouse/_indicators_signals.py +161 -0
- tradingkit_py-0.1.0/tradingkit/core/clickhouse/_plugin_library.py +43 -0
- tradingkit_py-0.1.0/tradingkit/core/clickhouse/_sql.py +138 -0
- tradingkit_py-0.1.0/tradingkit/core/clickhouse/_unit_tables.py +254 -0
- tradingkit_py-0.1.0/tradingkit/core/live_feed.py +78 -0
- tradingkit_py-0.1.0/tradingkit/core/params.py +148 -0
- tradingkit_py-0.1.0/tradingkit/core/plugin_registry.py +36 -0
- tradingkit_py-0.1.0/tradingkit/core/resolver.py +361 -0
- tradingkit_py-0.1.0/tradingkit/core/script_ast.py +56 -0
- tradingkit_py-0.1.0/tradingkit/core/timeframe.py +74 -0
- tradingkit_py-0.1.0/tradingkit/executor/__init__.py +23 -0
- tradingkit_py-0.1.0/tradingkit/executor/_worker.py +103 -0
- tradingkit_py-0.1.0/tradingkit/executor/base.py +82 -0
- tradingkit_py-0.1.0/tradingkit/executor/cpp_pool.py +161 -0
- tradingkit_py-0.1.0/tradingkit/executor/launchers/__init__.py +25 -0
- tradingkit_py-0.1.0/tradingkit/executor/launchers/base.py +44 -0
- tradingkit_py-0.1.0/tradingkit/executor/launchers/docker_.py +212 -0
- tradingkit_py-0.1.0/tradingkit/executor/launchers/subprocess_.py +89 -0
- tradingkit_py-0.1.0/tradingkit/executor/local.py +63 -0
- tradingkit_py-0.1.0/tradingkit/executor/remote.py +166 -0
- tradingkit_py-0.1.0/tradingkit/executor/subprocess_.py +189 -0
- tradingkit_py-0.1.0/tradingkit/indicator.py +520 -0
- tradingkit_py-0.1.0/tradingkit/pipeline.py +252 -0
- tradingkit_py-0.1.0/tradingkit/py.typed +0 -0
- tradingkit_py-0.1.0/tradingkit/runner/__init__.py +0 -0
- tradingkit_py-0.1.0/tradingkit/runner/_safe_pickle.py +94 -0
- tradingkit_py-0.1.0/tradingkit/runner/server.py +286 -0
- tradingkit_py-0.1.0/tradingkit/schema.py +292 -0
- tradingkit_py-0.1.0/tradingkit/source.py +259 -0
- tradingkit_py-0.1.0/tradingkit/strategy.py +380 -0
- tradingkit_py-0.1.0/tradingkit_py.egg-info/PKG-INFO +303 -0
- tradingkit_py-0.1.0/tradingkit_py.egg-info/SOURCES.txt +75 -0
- tradingkit_py-0.1.0/tradingkit_py.egg-info/dependency_links.txt +1 -0
- tradingkit_py-0.1.0/tradingkit_py.egg-info/entry_points.txt +2 -0
- tradingkit_py-0.1.0/tradingkit_py.egg-info/requires.txt +24 -0
- tradingkit_py-0.1.0/tradingkit_py.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maksym Holovin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tradingkit-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async trading framework with Polars, ClickHouse, and pluggable executors
|
|
5
|
+
Author-email: Maksym Holovin <holovin.maksym@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: aiohttp<4.0.0,>=3.9.0
|
|
11
|
+
Requires-Dist: aiohttp-jinja2==1.5.1
|
|
12
|
+
Requires-Dist: jinja2==3.1.2
|
|
13
|
+
Requires-Dist: polars>=0.20
|
|
14
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
15
|
+
Requires-Dist: numpy>=2.2.6
|
|
16
|
+
Requires-Dist: TA-Lib>=0.4.0
|
|
17
|
+
Requires-Dist: numba>=0.59.0
|
|
18
|
+
Requires-Dist: asynch>=0.2.0
|
|
19
|
+
Requires-Dist: pydantic<3.0.0,>=2.6.0
|
|
20
|
+
Requires-Dist: pyyaml<7.0.0,>=6.0.1
|
|
21
|
+
Requires-Dist: python-dateutil<3.0.0,>=2.8.2
|
|
22
|
+
Requires-Dist: pytz>=2023.3
|
|
23
|
+
Requires-Dist: rich<14.0.0,>=13.7.0
|
|
24
|
+
Requires-Dist: websockets<13.0,>=12.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.9; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
30
|
+
Provides-Extra: cpp
|
|
31
|
+
Requires-Dist: docker>=7.0.0; extra == "cpp"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# tradingkit
|
|
35
|
+
|
|
36
|
+
[](https://github.com/mholovion/trading-framework/actions/workflows/ci.yml)
|
|
37
|
+
|
|
38
|
+
Async trading strategy framework built on [Polars](https://pola.rs), [ClickHouse](https://clickhouse.com),
|
|
39
|
+
and pluggable executors. Write an `Indicator`/`Strategy`/`DataSource` once, then run it
|
|
40
|
+
in-process, in a sandboxed subprocess, on a remote worker, or against historical data in
|
|
41
|
+
a backtest — without changing the plugin code.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install tradingkit-py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
(PyPI distribution name is `tradingkit-py` — an unrelated project already holds the bare
|
|
52
|
+
`tradingkit` name — but the actual import is unaffected: `import tradingkit`.)
|
|
53
|
+
|
|
54
|
+
Requires Python ≥3.11. `TA-Lib` needs the native library installed first (see
|
|
55
|
+
[ta-lib.org](https://ta-lib.org)); everything else is a normal wheel dependency.
|
|
56
|
+
|
|
57
|
+
For development:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/mholovion/trading-framework.git
|
|
61
|
+
cd trading-framework
|
|
62
|
+
pip install -e ".[dev]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import asyncio
|
|
71
|
+
import numpy as np
|
|
72
|
+
import polars as pl
|
|
73
|
+
from tradingkit import Indicator, IndicatorContext, Strategy, Signal, BarContext
|
|
74
|
+
from tradingkit.backtest import BacktestRunner
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RSI(Indicator):
|
|
78
|
+
def compute(self, ctx: IndicatorContext) -> np.ndarray:
|
|
79
|
+
return ctx.ta.rsi(ctx.np.close, self.period)
|
|
80
|
+
|
|
81
|
+
def required_periods(self) -> int:
|
|
82
|
+
return self.period + 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class MeanReversion(Strategy):
|
|
86
|
+
async def on_bar(self, bar: BarContext) -> Signal | None:
|
|
87
|
+
# Signal.type is a free-form string in general, but BacktestRunner's
|
|
88
|
+
# FIFO trade pairing specifically recognizes "buy"/"sell" — use those
|
|
89
|
+
# two if you want trades in BacktestResult.trades.
|
|
90
|
+
if bar.rsi < 30:
|
|
91
|
+
return Signal("buy", confidence=0.8, metadata={"direction": "long"})
|
|
92
|
+
if bar.rsi > 70:
|
|
93
|
+
return Signal("sell", confidence=0.7)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def main() -> None:
|
|
98
|
+
data = pl.read_csv("candles.csv") # timestamp, open, high, low, close, volume
|
|
99
|
+
runner = BacktestRunner() # defaults to LocalExecutor (in-process, no isolation)
|
|
100
|
+
result = await runner.run(
|
|
101
|
+
data=data,
|
|
102
|
+
indicators={"rsi": RSI(period=14)},
|
|
103
|
+
strategy=MeanReversion(),
|
|
104
|
+
)
|
|
105
|
+
print(result.summary())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
asyncio.run(main())
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Architecture
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
DataSource ──► Pipeline ──► Indicator(s) ──► Strategy ──► Signal
|
|
117
|
+
│ │ │
|
|
118
|
+
└────────────── all three run through a PluginExecutor ──────────┘
|
|
119
|
+
(Local / Subprocess / Remote / CppRunnerPool)
|
|
120
|
+
|
|
121
|
+
ClickHouseManager + DependencyResolver (optional, application layer)
|
|
122
|
+
caches indicator/strategy output in ClickHouse and resolves
|
|
123
|
+
dependencies on demand instead of recomputing from scratch.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- **`DataSource`** ([tradingkit/source.py](tradingkit/source.py)) — fetches OHLCV data (exchange, CSV, custom API).
|
|
127
|
+
- **`Indicator`** ([tradingkit/indicator.py](tradingkit/indicator.py)) — `compute(ctx) -> np.ndarray`, given a `IndicatorContext` wrapping a `pl.DataFrame`. `ctx.ta.*` exposes 200+ TA-Lib functions plus numba-compiled primitives.
|
|
128
|
+
- **`Strategy`** ([tradingkit/strategy.py](tradingkit/strategy.py)) — `on_bar(bar) -> Signal | None`, given a `BarContext` with row + indicator values as dynamic attributes (`bar.close`, `bar.rsi`, ...).
|
|
129
|
+
- **`Pipeline`** ([tradingkit/pipeline.py](tradingkit/pipeline.py)) — wires a source + indicators + strategy into a single re-runnable, serializable unit.
|
|
130
|
+
- **`PluginExecutor`** ([tradingkit/executor/](tradingkit/executor/)) — where the above actually run:
|
|
131
|
+
|
|
132
|
+
| Executor | Isolation | Use case |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `LocalExecutor` | none | development, trusted plugins |
|
|
135
|
+
| `SubprocessExecutor` | separate process, Arrow IPC, memory/timeout limits | untrusted-ish plugins, single machine |
|
|
136
|
+
| `RemoteExecutor` | separate host over HTTP | horizontal scaling, dedicated compute nodes — see [Security model](#security-model) before exposing it beyond localhost |
|
|
137
|
+
| `CppRunnerPool` (attach to any of the above) | Docker, `--network=none`, seccomp, read-only rootfs | compiled `CppIndicator`/`CppStrategyPlugin` payloads |
|
|
138
|
+
|
|
139
|
+
- **`BacktestRunner`** ([tradingkit/backtest/](tradingkit/backtest/)) — runs a strategy bar-by-bar over a pre-loaded `pl.DataFrame` and returns a `BacktestResult` (trades, win rate, drawdown, ...).
|
|
140
|
+
- **`ClickHouseManager` + `DependencyResolver`** ([tradingkit/core/](tradingkit/core/)) — optional application-layer caching: resolves an indicator/strategy request by walking its dependency chain and only recomputing what's missing from ClickHouse.
|
|
141
|
+
- **`AggregationContext` / `AggregationWorker`** ([tradingkit/aggregation.py](tradingkit/aggregation.py)) — periodic scripts that read arbitrary ClickHouse tables and write derived series (cross-symbol spreads, higher-timeframe values projected onto a lower timeframe, etc.).
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Dynamic (string-based) plugins
|
|
146
|
+
|
|
147
|
+
`Indicator`/`Strategy`/`DataSource` subclasses are regular Python classes — defined in
|
|
148
|
+
your codebase, imported at process start. Sometimes the plugin code itself isn't known
|
|
149
|
+
until runtime instead (loaded from a database row, a config file, a user-facing editor):
|
|
150
|
+
`ScriptIndicator` / `ScriptStrategy` / `ScriptSource` / `AggregationScript` cover that case
|
|
151
|
+
by taking the plugin body as a plain string and `exec()`-ing it on demand, instead of
|
|
152
|
+
requiring a class defined ahead of time:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from tradingkit import ScriptIndicator
|
|
156
|
+
|
|
157
|
+
indicator = ScriptIndicator(code="""
|
|
158
|
+
result = ta.rsi(close, 14)
|
|
159
|
+
""", period=14)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Because this runs via `exec()` with no sandboxing, treat the `code` string with the same
|
|
163
|
+
trust as any other code you run — see [Security model](#security-model).
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Security model
|
|
168
|
+
|
|
169
|
+
tradingkit executes plugin code by design — that's the product. Two things are worth
|
|
170
|
+
being explicit about before you deploy it:
|
|
171
|
+
|
|
172
|
+
**1. In-process script execution has no sandbox.** `ScriptIndicator`, `ScriptStrategy`,
|
|
173
|
+
`ScriptSource`, and `AggregationScript` run their `code` string via `exec()` with full
|
|
174
|
+
interpreter privileges — no import restrictions, no resource limits. Only run code you
|
|
175
|
+
personally wrote or reviewed this way. If you need to run less-trusted plugin code, route
|
|
176
|
+
it through `SubprocessExecutor` (process boundary + memory/timeout limits) or
|
|
177
|
+
`CppRunnerPool` (Docker, `--network=none`, seccomp, read-only rootfs) instead of
|
|
178
|
+
`LocalExecutor`.
|
|
179
|
+
|
|
180
|
+
**2. `tradingkit-runner` requires a token and rejects non-loopback binds without one.**
|
|
181
|
+
`RemoteExecutor`/`tradingkit-runner` ship live plugin objects over the wire as pickle, so
|
|
182
|
+
the runner authenticates every `/compute/*` request (`Authorization: Bearer <token>`,
|
|
183
|
+
constant-time comparison, checked before the body is read) and decodes payloads with an
|
|
184
|
+
allowlisting unpickler (`tradingkit.runner._safe_pickle`) that blocks the standard
|
|
185
|
+
`os`/`subprocess`/`eval` pickle RCE gadgets even from an authenticated caller. Binding to
|
|
186
|
+
anything other than `127.0.0.1` requires both `--allow-remote` and a token — the process
|
|
187
|
+
refuses to start otherwise.
|
|
188
|
+
|
|
189
|
+
That said: **the token is a shared secret between trusted peers, not a full authz or
|
|
190
|
+
encryption layer.** Plain HTTP sends it in cleartext, and the restricted unpickler is a
|
|
191
|
+
targeted defense against known gadget classes, not a general-purpose sandbox. Don't put
|
|
192
|
+
`tradingkit-runner` on the public internet — run it behind a TLS-terminating reverse
|
|
193
|
+
proxy or keep it on a private network (VPN/VPC):
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
tradingkit-runner --token "$(openssl rand -hex 32)" # loopback only, default
|
|
197
|
+
tradingkit-runner --token "$TOKEN" --host 0.0.0.0 --allow-remote # only behind TLS/VPN
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
executor = RemoteExecutor("https://tradingkit-runner.internal:8082", token=TOKEN)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**3. `DockerRunnerLauncher` mounts `docker.sock`.** Whatever process can reach that socket
|
|
205
|
+
has effective root on the host — it's what lets the launcher build and start the sandboxed
|
|
206
|
+
C++ runner containers on first use. Treat access to the host running `tradingkit-runner`/
|
|
207
|
+
`CppRunnerPool` with that in mind; don't give untrusted users shell access to it.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Aggregation
|
|
212
|
+
|
|
213
|
+
`AggregationContext.query(table, symbol=..., start_ts=..., end_ts=...)` reads *any*
|
|
214
|
+
ClickHouse table by name — tradingkit ships no built-in aggregation scripts
|
|
215
|
+
(`BUILTIN_AGGREGATIONS` is an empty registry by default), but the mechanism covers a
|
|
216
|
+
few patterns directly:
|
|
217
|
+
|
|
218
|
+
- **Cross-symbol** — query two symbols, combine them (spread, ratio, custom index).
|
|
219
|
+
- **Cross-timeframe** — query a pre-aggregated table for a different bucket size (e.g.
|
|
220
|
+
`candles_3600s` for 1h buckets) and project it onto a lower-timeframe series; this is
|
|
221
|
+
the exact case `query()`'s own docstring calls out.
|
|
222
|
+
- **Cross-source** — the same `query()` call works on any table regardless of what wrote
|
|
223
|
+
it, so combining two independently-ingested tables (e.g. spot price + a funding-rate
|
|
224
|
+
feed) uses the same call shape. There's no bundled example of this one yet — you write
|
|
225
|
+
the join.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
OUTPUT_TABLE = "spread_btc_eth"
|
|
229
|
+
OUTPUT_SCHEMA = {"timestamp": "Int64", "spread": "Float64", "ratio": "Float64"}
|
|
230
|
+
INTERVAL_S = 60
|
|
231
|
+
|
|
232
|
+
async def aggregate(ctx, start_ts: int, end_ts: int) -> list[dict]:
|
|
233
|
+
btc = await ctx.query("candles", symbol="BTC_USDT", start_ts=start_ts, end_ts=end_ts)
|
|
234
|
+
eth = await ctx.query("candles", symbol="ETH_USDT", start_ts=start_ts, end_ts=end_ts)
|
|
235
|
+
joined = btc.join(eth, on="timestamp", suffix="_eth")
|
|
236
|
+
return [
|
|
237
|
+
{"timestamp": int(r["timestamp"]), "spread": r["close"] - r["close_eth"],
|
|
238
|
+
"ratio": r["close"] / r["close_eth"] if r["close_eth"] else 0.0}
|
|
239
|
+
for r in joined.iter_rows(named=True)
|
|
240
|
+
]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`AggregationWorker` polls `plugin_library` for scripts like this one (`type="aggregation"`,
|
|
244
|
+
defines `aggregate()`) and runs each on `INTERVAL_S`, writing results to `OUTPUT_TABLE`.
|
|
245
|
+
|
|
246
|
+
**Known limitation — parametric ClickHouse aggregates.** Separately from the Python-script
|
|
247
|
+
path above, `tradingkit.core.clickhouse` also has a lower-level, ClickHouse-native
|
|
248
|
+
aggregation path (`Fold` in `tradingkit/schema.py`, plus `ensure_agg_table`/`ensure_mv`/
|
|
249
|
+
`backfill_agg`/`query_agg`), backed by `AggregatingMergeTree` tables and materialized
|
|
250
|
+
views using ClickHouse's `-State`/`-Merge` combinators. Argument-less functions (`sum`,
|
|
251
|
+
`count`, `avg`, `min`, `max`, ...) generate correct SQL and are covered by integration
|
|
252
|
+
tests against a real server. Parametric functions (`quantile`, `sumIf`, ...) don't yet —
|
|
253
|
+
the combinator suffix gets placed incorrectly relative to the function's own arguments,
|
|
254
|
+
producing SQL ClickHouse won't accept as written. This needs an actual syntax-generation
|
|
255
|
+
fix, not a quick patch, and isn't done yet — don't rely on `Fold` with parametric
|
|
256
|
+
functions until this is resolved.
|
|
257
|
+
|
|
258
|
+
**Registering named builtins.** `plugin_library` (ClickHouse-stored, per-project) is the
|
|
259
|
+
primary way to add aggregation/strategy scripts, but a host app can additionally register
|
|
260
|
+
its own named builtins — e.g. a curated set it always wants available regardless of
|
|
261
|
+
project — by pointing an environment variable at a module:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
export TRADINGKIT_AGGREGATIONS_MODULE=myapp.aggregations # exposes BUILTIN_AGGREGATIONS: dict[str, str]
|
|
265
|
+
export TRADINGKIT_STRATEGIES_MODULE=myapp.strategies # exposes BUILTIN_STRATEGIES: dict[str, str]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Unset (the default), both registries are empty — see `tradingkit.core.plugin_registry`.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Development
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
pip install -e ".[dev]"
|
|
276
|
+
ruff check .
|
|
277
|
+
pytest
|
|
278
|
+
mypy tradingkit # non-blocking in CI — existing type-coverage gaps, not enforced yet
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
CI (`.github/workflows/ci.yml`) runs `ruff` and `pytest` on Python 3.11 and 3.12 for every
|
|
282
|
+
push/PR, including a real ClickHouse service container and (on Docker-capable runners) a
|
|
283
|
+
real `CppRunnerPool` container.
|
|
284
|
+
|
|
285
|
+
Most of the suite needs nothing beyond `pip install -e ".[dev]"`. Two groups of tests
|
|
286
|
+
auto-skip unless their dependency is actually reachable, and light up locally too if you
|
|
287
|
+
have it running:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# tests/test_clickhouse_integration.py — real ClickHouse instead of a mocked transport
|
|
291
|
+
docker run --rm -p 8123:8123 -p 9000:9000 clickhouse/clickhouse-server
|
|
292
|
+
CLICKHOUSE_HOST=localhost pytest tests/test_clickhouse_integration.py
|
|
293
|
+
|
|
294
|
+
# tests/test_cpp_pool.py — SubprocessRunnerLauncher tier needs only g++ (runs by default);
|
|
295
|
+
# the DockerRunnerLauncher tier additionally needs `pip install -e ".[dev,cpp]"` and Docker
|
|
296
|
+
pytest tests/test_cpp_pool.py
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# tradingkit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mholovion/trading-framework/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Async trading strategy framework built on [Polars](https://pola.rs), [ClickHouse](https://clickhouse.com),
|
|
6
|
+
and pluggable executors. Write an `Indicator`/`Strategy`/`DataSource` once, then run it
|
|
7
|
+
in-process, in a sandboxed subprocess, on a remote worker, or against historical data in
|
|
8
|
+
a backtest — without changing the plugin code.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install tradingkit-py
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
(PyPI distribution name is `tradingkit-py` — an unrelated project already holds the bare
|
|
19
|
+
`tradingkit` name — but the actual import is unaffected: `import tradingkit`.)
|
|
20
|
+
|
|
21
|
+
Requires Python ≥3.11. `TA-Lib` needs the native library installed first (see
|
|
22
|
+
[ta-lib.org](https://ta-lib.org)); everything else is a normal wheel dependency.
|
|
23
|
+
|
|
24
|
+
For development:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone https://github.com/mholovion/trading-framework.git
|
|
28
|
+
cd trading-framework
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
import numpy as np
|
|
39
|
+
import polars as pl
|
|
40
|
+
from tradingkit import Indicator, IndicatorContext, Strategy, Signal, BarContext
|
|
41
|
+
from tradingkit.backtest import BacktestRunner
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RSI(Indicator):
|
|
45
|
+
def compute(self, ctx: IndicatorContext) -> np.ndarray:
|
|
46
|
+
return ctx.ta.rsi(ctx.np.close, self.period)
|
|
47
|
+
|
|
48
|
+
def required_periods(self) -> int:
|
|
49
|
+
return self.period + 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MeanReversion(Strategy):
|
|
53
|
+
async def on_bar(self, bar: BarContext) -> Signal | None:
|
|
54
|
+
# Signal.type is a free-form string in general, but BacktestRunner's
|
|
55
|
+
# FIFO trade pairing specifically recognizes "buy"/"sell" — use those
|
|
56
|
+
# two if you want trades in BacktestResult.trades.
|
|
57
|
+
if bar.rsi < 30:
|
|
58
|
+
return Signal("buy", confidence=0.8, metadata={"direction": "long"})
|
|
59
|
+
if bar.rsi > 70:
|
|
60
|
+
return Signal("sell", confidence=0.7)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def main() -> None:
|
|
65
|
+
data = pl.read_csv("candles.csv") # timestamp, open, high, low, close, volume
|
|
66
|
+
runner = BacktestRunner() # defaults to LocalExecutor (in-process, no isolation)
|
|
67
|
+
result = await runner.run(
|
|
68
|
+
data=data,
|
|
69
|
+
indicators={"rsi": RSI(period=14)},
|
|
70
|
+
strategy=MeanReversion(),
|
|
71
|
+
)
|
|
72
|
+
print(result.summary())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
asyncio.run(main())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Architecture
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
DataSource ──► Pipeline ──► Indicator(s) ──► Strategy ──► Signal
|
|
84
|
+
│ │ │
|
|
85
|
+
└────────────── all three run through a PluginExecutor ──────────┘
|
|
86
|
+
(Local / Subprocess / Remote / CppRunnerPool)
|
|
87
|
+
|
|
88
|
+
ClickHouseManager + DependencyResolver (optional, application layer)
|
|
89
|
+
caches indicator/strategy output in ClickHouse and resolves
|
|
90
|
+
dependencies on demand instead of recomputing from scratch.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **`DataSource`** ([tradingkit/source.py](tradingkit/source.py)) — fetches OHLCV data (exchange, CSV, custom API).
|
|
94
|
+
- **`Indicator`** ([tradingkit/indicator.py](tradingkit/indicator.py)) — `compute(ctx) -> np.ndarray`, given a `IndicatorContext` wrapping a `pl.DataFrame`. `ctx.ta.*` exposes 200+ TA-Lib functions plus numba-compiled primitives.
|
|
95
|
+
- **`Strategy`** ([tradingkit/strategy.py](tradingkit/strategy.py)) — `on_bar(bar) -> Signal | None`, given a `BarContext` with row + indicator values as dynamic attributes (`bar.close`, `bar.rsi`, ...).
|
|
96
|
+
- **`Pipeline`** ([tradingkit/pipeline.py](tradingkit/pipeline.py)) — wires a source + indicators + strategy into a single re-runnable, serializable unit.
|
|
97
|
+
- **`PluginExecutor`** ([tradingkit/executor/](tradingkit/executor/)) — where the above actually run:
|
|
98
|
+
|
|
99
|
+
| Executor | Isolation | Use case |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `LocalExecutor` | none | development, trusted plugins |
|
|
102
|
+
| `SubprocessExecutor` | separate process, Arrow IPC, memory/timeout limits | untrusted-ish plugins, single machine |
|
|
103
|
+
| `RemoteExecutor` | separate host over HTTP | horizontal scaling, dedicated compute nodes — see [Security model](#security-model) before exposing it beyond localhost |
|
|
104
|
+
| `CppRunnerPool` (attach to any of the above) | Docker, `--network=none`, seccomp, read-only rootfs | compiled `CppIndicator`/`CppStrategyPlugin` payloads |
|
|
105
|
+
|
|
106
|
+
- **`BacktestRunner`** ([tradingkit/backtest/](tradingkit/backtest/)) — runs a strategy bar-by-bar over a pre-loaded `pl.DataFrame` and returns a `BacktestResult` (trades, win rate, drawdown, ...).
|
|
107
|
+
- **`ClickHouseManager` + `DependencyResolver`** ([tradingkit/core/](tradingkit/core/)) — optional application-layer caching: resolves an indicator/strategy request by walking its dependency chain and only recomputing what's missing from ClickHouse.
|
|
108
|
+
- **`AggregationContext` / `AggregationWorker`** ([tradingkit/aggregation.py](tradingkit/aggregation.py)) — periodic scripts that read arbitrary ClickHouse tables and write derived series (cross-symbol spreads, higher-timeframe values projected onto a lower timeframe, etc.).
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Dynamic (string-based) plugins
|
|
113
|
+
|
|
114
|
+
`Indicator`/`Strategy`/`DataSource` subclasses are regular Python classes — defined in
|
|
115
|
+
your codebase, imported at process start. Sometimes the plugin code itself isn't known
|
|
116
|
+
until runtime instead (loaded from a database row, a config file, a user-facing editor):
|
|
117
|
+
`ScriptIndicator` / `ScriptStrategy` / `ScriptSource` / `AggregationScript` cover that case
|
|
118
|
+
by taking the plugin body as a plain string and `exec()`-ing it on demand, instead of
|
|
119
|
+
requiring a class defined ahead of time:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from tradingkit import ScriptIndicator
|
|
123
|
+
|
|
124
|
+
indicator = ScriptIndicator(code="""
|
|
125
|
+
result = ta.rsi(close, 14)
|
|
126
|
+
""", period=14)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Because this runs via `exec()` with no sandboxing, treat the `code` string with the same
|
|
130
|
+
trust as any other code you run — see [Security model](#security-model).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Security model
|
|
135
|
+
|
|
136
|
+
tradingkit executes plugin code by design — that's the product. Two things are worth
|
|
137
|
+
being explicit about before you deploy it:
|
|
138
|
+
|
|
139
|
+
**1. In-process script execution has no sandbox.** `ScriptIndicator`, `ScriptStrategy`,
|
|
140
|
+
`ScriptSource`, and `AggregationScript` run their `code` string via `exec()` with full
|
|
141
|
+
interpreter privileges — no import restrictions, no resource limits. Only run code you
|
|
142
|
+
personally wrote or reviewed this way. If you need to run less-trusted plugin code, route
|
|
143
|
+
it through `SubprocessExecutor` (process boundary + memory/timeout limits) or
|
|
144
|
+
`CppRunnerPool` (Docker, `--network=none`, seccomp, read-only rootfs) instead of
|
|
145
|
+
`LocalExecutor`.
|
|
146
|
+
|
|
147
|
+
**2. `tradingkit-runner` requires a token and rejects non-loopback binds without one.**
|
|
148
|
+
`RemoteExecutor`/`tradingkit-runner` ship live plugin objects over the wire as pickle, so
|
|
149
|
+
the runner authenticates every `/compute/*` request (`Authorization: Bearer <token>`,
|
|
150
|
+
constant-time comparison, checked before the body is read) and decodes payloads with an
|
|
151
|
+
allowlisting unpickler (`tradingkit.runner._safe_pickle`) that blocks the standard
|
|
152
|
+
`os`/`subprocess`/`eval` pickle RCE gadgets even from an authenticated caller. Binding to
|
|
153
|
+
anything other than `127.0.0.1` requires both `--allow-remote` and a token — the process
|
|
154
|
+
refuses to start otherwise.
|
|
155
|
+
|
|
156
|
+
That said: **the token is a shared secret between trusted peers, not a full authz or
|
|
157
|
+
encryption layer.** Plain HTTP sends it in cleartext, and the restricted unpickler is a
|
|
158
|
+
targeted defense against known gadget classes, not a general-purpose sandbox. Don't put
|
|
159
|
+
`tradingkit-runner` on the public internet — run it behind a TLS-terminating reverse
|
|
160
|
+
proxy or keep it on a private network (VPN/VPC):
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
tradingkit-runner --token "$(openssl rand -hex 32)" # loopback only, default
|
|
164
|
+
tradingkit-runner --token "$TOKEN" --host 0.0.0.0 --allow-remote # only behind TLS/VPN
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
executor = RemoteExecutor("https://tradingkit-runner.internal:8082", token=TOKEN)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**3. `DockerRunnerLauncher` mounts `docker.sock`.** Whatever process can reach that socket
|
|
172
|
+
has effective root on the host — it's what lets the launcher build and start the sandboxed
|
|
173
|
+
C++ runner containers on first use. Treat access to the host running `tradingkit-runner`/
|
|
174
|
+
`CppRunnerPool` with that in mind; don't give untrusted users shell access to it.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Aggregation
|
|
179
|
+
|
|
180
|
+
`AggregationContext.query(table, symbol=..., start_ts=..., end_ts=...)` reads *any*
|
|
181
|
+
ClickHouse table by name — tradingkit ships no built-in aggregation scripts
|
|
182
|
+
(`BUILTIN_AGGREGATIONS` is an empty registry by default), but the mechanism covers a
|
|
183
|
+
few patterns directly:
|
|
184
|
+
|
|
185
|
+
- **Cross-symbol** — query two symbols, combine them (spread, ratio, custom index).
|
|
186
|
+
- **Cross-timeframe** — query a pre-aggregated table for a different bucket size (e.g.
|
|
187
|
+
`candles_3600s` for 1h buckets) and project it onto a lower-timeframe series; this is
|
|
188
|
+
the exact case `query()`'s own docstring calls out.
|
|
189
|
+
- **Cross-source** — the same `query()` call works on any table regardless of what wrote
|
|
190
|
+
it, so combining two independently-ingested tables (e.g. spot price + a funding-rate
|
|
191
|
+
feed) uses the same call shape. There's no bundled example of this one yet — you write
|
|
192
|
+
the join.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
OUTPUT_TABLE = "spread_btc_eth"
|
|
196
|
+
OUTPUT_SCHEMA = {"timestamp": "Int64", "spread": "Float64", "ratio": "Float64"}
|
|
197
|
+
INTERVAL_S = 60
|
|
198
|
+
|
|
199
|
+
async def aggregate(ctx, start_ts: int, end_ts: int) -> list[dict]:
|
|
200
|
+
btc = await ctx.query("candles", symbol="BTC_USDT", start_ts=start_ts, end_ts=end_ts)
|
|
201
|
+
eth = await ctx.query("candles", symbol="ETH_USDT", start_ts=start_ts, end_ts=end_ts)
|
|
202
|
+
joined = btc.join(eth, on="timestamp", suffix="_eth")
|
|
203
|
+
return [
|
|
204
|
+
{"timestamp": int(r["timestamp"]), "spread": r["close"] - r["close_eth"],
|
|
205
|
+
"ratio": r["close"] / r["close_eth"] if r["close_eth"] else 0.0}
|
|
206
|
+
for r in joined.iter_rows(named=True)
|
|
207
|
+
]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`AggregationWorker` polls `plugin_library` for scripts like this one (`type="aggregation"`,
|
|
211
|
+
defines `aggregate()`) and runs each on `INTERVAL_S`, writing results to `OUTPUT_TABLE`.
|
|
212
|
+
|
|
213
|
+
**Known limitation — parametric ClickHouse aggregates.** Separately from the Python-script
|
|
214
|
+
path above, `tradingkit.core.clickhouse` also has a lower-level, ClickHouse-native
|
|
215
|
+
aggregation path (`Fold` in `tradingkit/schema.py`, plus `ensure_agg_table`/`ensure_mv`/
|
|
216
|
+
`backfill_agg`/`query_agg`), backed by `AggregatingMergeTree` tables and materialized
|
|
217
|
+
views using ClickHouse's `-State`/`-Merge` combinators. Argument-less functions (`sum`,
|
|
218
|
+
`count`, `avg`, `min`, `max`, ...) generate correct SQL and are covered by integration
|
|
219
|
+
tests against a real server. Parametric functions (`quantile`, `sumIf`, ...) don't yet —
|
|
220
|
+
the combinator suffix gets placed incorrectly relative to the function's own arguments,
|
|
221
|
+
producing SQL ClickHouse won't accept as written. This needs an actual syntax-generation
|
|
222
|
+
fix, not a quick patch, and isn't done yet — don't rely on `Fold` with parametric
|
|
223
|
+
functions until this is resolved.
|
|
224
|
+
|
|
225
|
+
**Registering named builtins.** `plugin_library` (ClickHouse-stored, per-project) is the
|
|
226
|
+
primary way to add aggregation/strategy scripts, but a host app can additionally register
|
|
227
|
+
its own named builtins — e.g. a curated set it always wants available regardless of
|
|
228
|
+
project — by pointing an environment variable at a module:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
export TRADINGKIT_AGGREGATIONS_MODULE=myapp.aggregations # exposes BUILTIN_AGGREGATIONS: dict[str, str]
|
|
232
|
+
export TRADINGKIT_STRATEGIES_MODULE=myapp.strategies # exposes BUILTIN_STRATEGIES: dict[str, str]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Unset (the default), both registries are empty — see `tradingkit.core.plugin_registry`.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
pip install -e ".[dev]"
|
|
243
|
+
ruff check .
|
|
244
|
+
pytest
|
|
245
|
+
mypy tradingkit # non-blocking in CI — existing type-coverage gaps, not enforced yet
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
CI (`.github/workflows/ci.yml`) runs `ruff` and `pytest` on Python 3.11 and 3.12 for every
|
|
249
|
+
push/PR, including a real ClickHouse service container and (on Docker-capable runners) a
|
|
250
|
+
real `CppRunnerPool` container.
|
|
251
|
+
|
|
252
|
+
Most of the suite needs nothing beyond `pip install -e ".[dev]"`. Two groups of tests
|
|
253
|
+
auto-skip unless their dependency is actually reachable, and light up locally too if you
|
|
254
|
+
have it running:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# tests/test_clickhouse_integration.py — real ClickHouse instead of a mocked transport
|
|
258
|
+
docker run --rm -p 8123:8123 -p 9000:9000 clickhouse/clickhouse-server
|
|
259
|
+
CLICKHOUSE_HOST=localhost pytest tests/test_clickhouse_integration.py
|
|
260
|
+
|
|
261
|
+
# tests/test_cpp_pool.py — SubprocessRunnerLauncher tier needs only g++ (runs by default);
|
|
262
|
+
# the DockerRunnerLauncher tier additionally needs `pip install -e ".[dev,cpp]"` and Docker
|
|
263
|
+
pytest tests/test_cpp_pool.py
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# Distribution name for PyPI ("tradingkit" is already taken by an unrelated package) --
|
|
7
|
+
# the importable package is still `tradingkit` (see [tool.setuptools.packages.find]
|
|
8
|
+
# below), so `import tradingkit` in every example stays unchanged.
|
|
9
|
+
name = "tradingkit-py"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
description = "Async trading framework with Polars, ClickHouse, and pluggable executors"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Maksym Holovin", email = "holovin.maksym@gmail.com" },
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"aiohttp>=3.9.0,<4.0.0",
|
|
20
|
+
"aiohttp-jinja2==1.5.1",
|
|
21
|
+
"jinja2==3.1.2",
|
|
22
|
+
"polars>=0.20",
|
|
23
|
+
"pyarrow>=14.0.0",
|
|
24
|
+
"numpy>=2.2.6",
|
|
25
|
+
"TA-Lib>=0.4.0",
|
|
26
|
+
"numba>=0.59.0",
|
|
27
|
+
"asynch>=0.2.0",
|
|
28
|
+
"pydantic>=2.6.0,<3.0.0",
|
|
29
|
+
"pyyaml>=6.0.1,<7.0.0",
|
|
30
|
+
"python-dateutil>=2.8.2,<3.0.0",
|
|
31
|
+
"pytz>=2023.3",
|
|
32
|
+
"rich>=13.7.0,<14.0.0",
|
|
33
|
+
"websockets>=12.0,<13.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23", "mypy>=1.9", "ruff>=0.4"]
|
|
38
|
+
cpp = ["docker>=7.0.0"]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
tradingkit-runner = "tradingkit.runner.server:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["."]
|
|
45
|
+
include = ["tradingkit*"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
"tradingkit" = [
|
|
49
|
+
"py.typed",
|
|
50
|
+
"_docker/runner/Dockerfile",
|
|
51
|
+
"_docker/runner/runner_worker.py",
|
|
52
|
+
"_docker/runner/seccomp.json",
|
|
53
|
+
"_docker/compiler/Dockerfile",
|
|
54
|
+
"_docker/compiler/compile.sh",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 100
|
|
59
|
+
target-version = "py311"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
# E702 (semicolon-joined statements) is a deliberate, consistent style throughout the
|
|
63
|
+
# ClickHouse query-building methods: `if x is not None: where += "..."; params["k"] = x`
|
|
64
|
+
# keeps each optional filter to one line. Not a formatting slip — leave it alone.
|
|
65
|
+
ignore = ["E702"]
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
python_version = "3.11"
|
|
69
|
+
strict = false
|
|
70
|
+
ignore_missing_imports = true
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
testpaths = ["tests"]
|
|
74
|
+
asyncio_mode = "auto"
|