tradingkit-py 0.1.0__py3-none-any.whl
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/__init__.py +147 -0
- tradingkit/_docker/compiler/Dockerfile +6 -0
- tradingkit/_docker/compiler/compile.sh +19 -0
- tradingkit/_docker/runner/Dockerfile +7 -0
- tradingkit/_docker/runner/runner_worker.py +143 -0
- tradingkit/_docker/runner/seccomp.json +44 -0
- tradingkit/aggregation.py +329 -0
- tradingkit/backtest/__init__.py +4 -0
- tradingkit/backtest/result.py +100 -0
- tradingkit/backtest/runner.py +133 -0
- tradingkit/collector.py +572 -0
- tradingkit/context.py +38 -0
- tradingkit/core/__init__.py +0 -0
- tradingkit/core/clickhouse/__init__.py +136 -0
- tradingkit/core/clickhouse/_candles.py +234 -0
- tradingkit/core/clickhouse/_connections.py +90 -0
- tradingkit/core/clickhouse/_indicators_signals.py +161 -0
- tradingkit/core/clickhouse/_plugin_library.py +43 -0
- tradingkit/core/clickhouse/_sql.py +138 -0
- tradingkit/core/clickhouse/_unit_tables.py +254 -0
- tradingkit/core/live_feed.py +78 -0
- tradingkit/core/params.py +148 -0
- tradingkit/core/plugin_registry.py +36 -0
- tradingkit/core/resolver.py +361 -0
- tradingkit/core/script_ast.py +56 -0
- tradingkit/core/timeframe.py +74 -0
- tradingkit/executor/__init__.py +23 -0
- tradingkit/executor/_worker.py +103 -0
- tradingkit/executor/base.py +82 -0
- tradingkit/executor/cpp_pool.py +161 -0
- tradingkit/executor/launchers/__init__.py +25 -0
- tradingkit/executor/launchers/base.py +44 -0
- tradingkit/executor/launchers/docker_.py +212 -0
- tradingkit/executor/launchers/subprocess_.py +89 -0
- tradingkit/executor/local.py +63 -0
- tradingkit/executor/remote.py +166 -0
- tradingkit/executor/subprocess_.py +189 -0
- tradingkit/indicator.py +520 -0
- tradingkit/pipeline.py +252 -0
- tradingkit/py.typed +0 -0
- tradingkit/runner/__init__.py +0 -0
- tradingkit/runner/_safe_pickle.py +94 -0
- tradingkit/runner/server.py +286 -0
- tradingkit/schema.py +292 -0
- tradingkit/source.py +259 -0
- tradingkit/strategy.py +380 -0
- tradingkit_py-0.1.0.dist-info/METADATA +303 -0
- tradingkit_py-0.1.0.dist-info/RECORD +52 -0
- tradingkit_py-0.1.0.dist-info/WHEEL +5 -0
- tradingkit_py-0.1.0.dist-info/entry_points.txt +2 -0
- tradingkit_py-0.1.0.dist-info/licenses/LICENSE +21 -0
- tradingkit_py-0.1.0.dist-info/top_level.txt +1 -0
tradingkit/__init__.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tradingkit — pip-installable trading strategy framework.
|
|
3
|
+
|
|
4
|
+
Quick start:
|
|
5
|
+
from tradingkit import Indicator, Strategy, Signal, BarContext, ta_indicator
|
|
6
|
+
|
|
7
|
+
Writing an indicator:
|
|
8
|
+
from tradingkit import Indicator, IndicatorContext
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
class WeightedRSI(Indicator):
|
|
12
|
+
def compute(self, ctx: IndicatorContext) -> np.ndarray:
|
|
13
|
+
rsi = ctx.ta.rsi(ctx.np.close, self.period)
|
|
14
|
+
return rsi * (ctx.np.volume / ctx.np.volume.mean())
|
|
15
|
+
def required_periods(self) -> int:
|
|
16
|
+
return self.period + 1
|
|
17
|
+
|
|
18
|
+
Writing a strategy:
|
|
19
|
+
from tradingkit import Strategy, Signal, BarContext, ta_indicator
|
|
20
|
+
|
|
21
|
+
class MyStrategy(Strategy):
|
|
22
|
+
rsi = ta_indicator.rsi(period=14)
|
|
23
|
+
ema = ta_indicator.ema(period=50)
|
|
24
|
+
|
|
25
|
+
async def on_bar(self, bar: BarContext) -> Signal | None:
|
|
26
|
+
if bar.rsi < 30 and bar.close < bar.ema:
|
|
27
|
+
return Signal("entry", 0.8, metadata={"direction": "long"})
|
|
28
|
+
if bar.rsi > 70:
|
|
29
|
+
return Signal("exit", 0.7)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# ── Indicator ────────────────────────────────────────────────────────
|
|
33
|
+
from tradingkit.indicator import (
|
|
34
|
+
Indicator,
|
|
35
|
+
IndicatorContext,
|
|
36
|
+
IndicatorDeclaration,
|
|
37
|
+
ScriptIndicator,
|
|
38
|
+
JitIndicator,
|
|
39
|
+
CppIndicator,
|
|
40
|
+
ta_indicator,
|
|
41
|
+
load_indicator_plugin,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# ── Strategy ─────────────────────────────────────────────────────────
|
|
45
|
+
from tradingkit.strategy import (
|
|
46
|
+
Strategy,
|
|
47
|
+
Signal,
|
|
48
|
+
BarContext,
|
|
49
|
+
ScriptStrategy,
|
|
50
|
+
CppStrategyPlugin,
|
|
51
|
+
load_strategy_plugin,
|
|
52
|
+
StrategyPlugin,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# ── Source ───────────────────────────────────────────────────────────
|
|
56
|
+
from tradingkit.source import (
|
|
57
|
+
DataSource,
|
|
58
|
+
ScriptSource,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# ── Executor ─────────────────────────────────────────────────────────
|
|
62
|
+
from tradingkit.executor import (
|
|
63
|
+
PluginExecutor,
|
|
64
|
+
LocalExecutor,
|
|
65
|
+
SubprocessExecutor,
|
|
66
|
+
RemoteExecutor,
|
|
67
|
+
CppRunnerPool,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# ── Pipeline ─────────────────────────────────────────────────────────
|
|
71
|
+
from tradingkit.pipeline import (
|
|
72
|
+
Pipeline,
|
|
73
|
+
PipelineResult,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# ── Backtest ─────────────────────────────────────────────────────────
|
|
77
|
+
from tradingkit.backtest import (
|
|
78
|
+
BacktestRunner,
|
|
79
|
+
BacktestResult,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# ── Collector ────────────────────────────────────────────────────────
|
|
83
|
+
from tradingkit.collector import DataCollector
|
|
84
|
+
|
|
85
|
+
# ── Aggregation ──────────────────────────────────────────────────────
|
|
86
|
+
from tradingkit.aggregation import (
|
|
87
|
+
AggregationContext,
|
|
88
|
+
AggregationWorker,
|
|
89
|
+
AggregationScript,
|
|
90
|
+
load_aggregation_script,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# ── Application layer ────────────────────────────────────────────────
|
|
94
|
+
from tradingkit.context import TradingContext
|
|
95
|
+
from tradingkit.core.clickhouse import ClickHouseManager
|
|
96
|
+
from tradingkit.core.resolver import DependencyResolver
|
|
97
|
+
from tradingkit.core.timeframe import parse_timeframe
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__version__ = "0.1.0"
|
|
101
|
+
|
|
102
|
+
__all__ = [
|
|
103
|
+
# Indicator
|
|
104
|
+
"Indicator",
|
|
105
|
+
"IndicatorContext",
|
|
106
|
+
"IndicatorDeclaration",
|
|
107
|
+
"ScriptIndicator",
|
|
108
|
+
"JitIndicator",
|
|
109
|
+
"CppIndicator",
|
|
110
|
+
"ta_indicator",
|
|
111
|
+
"load_indicator_plugin",
|
|
112
|
+
# Strategy
|
|
113
|
+
"Strategy",
|
|
114
|
+
"Signal",
|
|
115
|
+
"BarContext",
|
|
116
|
+
"ScriptStrategy",
|
|
117
|
+
"CppStrategyPlugin",
|
|
118
|
+
"load_strategy_plugin",
|
|
119
|
+
"StrategyPlugin",
|
|
120
|
+
# Source
|
|
121
|
+
"DataSource",
|
|
122
|
+
"ScriptSource",
|
|
123
|
+
# Executor
|
|
124
|
+
"PluginExecutor",
|
|
125
|
+
"LocalExecutor",
|
|
126
|
+
"SubprocessExecutor",
|
|
127
|
+
"RemoteExecutor",
|
|
128
|
+
"CppRunnerPool",
|
|
129
|
+
# Pipeline
|
|
130
|
+
"Pipeline",
|
|
131
|
+
"PipelineResult",
|
|
132
|
+
# Backtest
|
|
133
|
+
"BacktestRunner",
|
|
134
|
+
"BacktestResult",
|
|
135
|
+
# Collector
|
|
136
|
+
"DataCollector",
|
|
137
|
+
# Aggregation
|
|
138
|
+
"AggregationContext",
|
|
139
|
+
"AggregationWorker",
|
|
140
|
+
"AggregationScript",
|
|
141
|
+
"load_aggregation_script",
|
|
142
|
+
# Application
|
|
143
|
+
"TradingContext",
|
|
144
|
+
"ClickHouseManager",
|
|
145
|
+
"DependencyResolver",
|
|
146
|
+
"parse_timeframe",
|
|
147
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Read C++ source from stdin, compile to .so, write to stdout.
|
|
3
|
+
# Args: [std] [opt] — defaults: c++20, -O2
|
|
4
|
+
# Output on success: "SUCCESS\n" followed by raw .so bytes
|
|
5
|
+
# Output on failure: "ERROR\n" followed by GCC stderr
|
|
6
|
+
|
|
7
|
+
STD=${1:-c++20}
|
|
8
|
+
OPT=${2:--O2}
|
|
9
|
+
|
|
10
|
+
cat > /tmp/indicator.cpp
|
|
11
|
+
g++ -shared -fPIC -std="$STD" "$OPT" -o /tmp/indicator.so /tmp/indicator.cpp 2>/tmp/gcc_stderr.txt
|
|
12
|
+
|
|
13
|
+
if [ $? -eq 0 ]; then
|
|
14
|
+
printf "SUCCESS\n"
|
|
15
|
+
cat /tmp/indicator.so
|
|
16
|
+
else
|
|
17
|
+
printf "ERROR\n"
|
|
18
|
+
cat /tmp/gcc_stderr.txt
|
|
19
|
+
fi
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
FROM debian:12-slim
|
|
2
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
3
|
+
python3 python3-pip \
|
|
4
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
5
|
+
RUN pip3 install --no-cache-dir --break-system-packages pyarrow numpy
|
|
6
|
+
COPY runner_worker.py /runner_worker.py
|
|
7
|
+
ENTRYPOINT ["python3", "/runner_worker.py"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
C++ indicator runner — Unix socket server.
|
|
4
|
+
|
|
5
|
+
Listens on /run/cpp-runner/runner-{RUNNER_ID}.sock.
|
|
6
|
+
Accepts length-prefixed pickle requests, executes compiled .so, returns results.
|
|
7
|
+
|
|
8
|
+
Protocol (same as CppRunnerPool):
|
|
9
|
+
Request: {"so": bytes, "candles": arrow_ipc_bytes, "params": json_str}
|
|
10
|
+
Response: {"values": float64_bytes} | {"error": str}
|
|
11
|
+
|
|
12
|
+
ABI expected from the .so:
|
|
13
|
+
extern "C" void indicator_compute(
|
|
14
|
+
const double* close, const double* high, const double* low,
|
|
15
|
+
const double* open, const double* volume,
|
|
16
|
+
int length, double* result, const char* params_json);
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import ctypes
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import pickle
|
|
25
|
+
import struct
|
|
26
|
+
import tempfile
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import pyarrow as pa
|
|
30
|
+
|
|
31
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
SOCKET_DIR = os.environ.get("RUNNER_SOCKET_DIR", "/run/cpp-runner")
|
|
35
|
+
RUNNER_ID = os.environ.get("RUNNER_ID", "0")
|
|
36
|
+
SOCK_PATH = f"{SOCKET_DIR}/runner-{RUNNER_ID}.sock"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_so(so_bytes: bytes) -> ctypes.CDLL:
|
|
40
|
+
with tempfile.NamedTemporaryFile(suffix=".so", delete=False) as f:
|
|
41
|
+
f.write(so_bytes)
|
|
42
|
+
f.flush()
|
|
43
|
+
return ctypes.CDLL(f.name)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_indicator(so_bytes: bytes, candles_ipc: bytes, params_json: str) -> bytes:
|
|
47
|
+
buf = pa.py_buffer(candles_ipc)
|
|
48
|
+
reader = pa.ipc.open_stream(buf)
|
|
49
|
+
table = reader.read_all()
|
|
50
|
+
|
|
51
|
+
def _col(name: str) -> np.ndarray:
|
|
52
|
+
return np.asarray(table.column(name), dtype=np.float64)
|
|
53
|
+
|
|
54
|
+
close = _col("close")
|
|
55
|
+
high = _col("high")
|
|
56
|
+
low = _col("low")
|
|
57
|
+
open_ = _col("open")
|
|
58
|
+
volume = _col("volume")
|
|
59
|
+
length = len(close)
|
|
60
|
+
result = np.zeros(length, dtype=np.float64)
|
|
61
|
+
|
|
62
|
+
lib = _load_so(so_bytes)
|
|
63
|
+
fn = lib.indicator_compute
|
|
64
|
+
fn.restype = None
|
|
65
|
+
fn.argtypes = [
|
|
66
|
+
ctypes.POINTER(ctypes.c_double), # close
|
|
67
|
+
ctypes.POINTER(ctypes.c_double), # high
|
|
68
|
+
ctypes.POINTER(ctypes.c_double), # low
|
|
69
|
+
ctypes.POINTER(ctypes.c_double), # open
|
|
70
|
+
ctypes.POINTER(ctypes.c_double), # volume
|
|
71
|
+
ctypes.c_int, # length
|
|
72
|
+
ctypes.POINTER(ctypes.c_double), # result
|
|
73
|
+
ctypes.c_char_p, # params_json
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
fn(
|
|
77
|
+
close.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
|
|
78
|
+
high.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
|
|
79
|
+
low.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
|
|
80
|
+
open_.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
|
|
81
|
+
volume.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
|
|
82
|
+
ctypes.c_int(length),
|
|
83
|
+
result.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
|
|
84
|
+
params_json.encode(),
|
|
85
|
+
)
|
|
86
|
+
return result.tobytes()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
90
|
+
try:
|
|
91
|
+
while True:
|
|
92
|
+
header = await reader.readexactly(4)
|
|
93
|
+
payload_len = struct.unpack(">I", header)[0]
|
|
94
|
+
raw = await reader.readexactly(payload_len)
|
|
95
|
+
req = pickle.loads(raw)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
values_bytes = _run_indicator(
|
|
99
|
+
req["so"],
|
|
100
|
+
req["data"],
|
|
101
|
+
req.get("params", "{}"),
|
|
102
|
+
)
|
|
103
|
+
response = {"values": values_bytes}
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
logger.exception("indicator_compute failed")
|
|
106
|
+
response = {"error": str(exc)}
|
|
107
|
+
|
|
108
|
+
resp_bytes = pickle.dumps(response)
|
|
109
|
+
writer.write(struct.pack(">I", len(resp_bytes)) + resp_bytes)
|
|
110
|
+
await writer.drain()
|
|
111
|
+
except (asyncio.IncompleteReadError, ConnectionResetError):
|
|
112
|
+
pass
|
|
113
|
+
finally:
|
|
114
|
+
writer.close()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def main() -> None:
|
|
118
|
+
os.makedirs(SOCKET_DIR, exist_ok=True)
|
|
119
|
+
if os.path.exists(SOCK_PATH):
|
|
120
|
+
os.unlink(SOCK_PATH)
|
|
121
|
+
|
|
122
|
+
server = await asyncio.start_unix_server(handle_client, path=SOCK_PATH)
|
|
123
|
+
try:
|
|
124
|
+
# 0o666, not 0o660: this container runs as root, but whoever connects doesn't
|
|
125
|
+
# necessarily -- the app container's UID in production, or a bare non-root CI
|
|
126
|
+
# runner process in tests (neither of which this image controls or can predict).
|
|
127
|
+
# 660 would restrict the socket to root:root, so anything not running as root
|
|
128
|
+
# gets EACCES on connect(). The real isolation boundary is network=none + seccomp
|
|
129
|
+
# + which processes can even see this shared path at all, not the socket's own
|
|
130
|
+
# owner/group bits.
|
|
131
|
+
os.chmod(SOCK_PATH, 0o666)
|
|
132
|
+
except OSError as exc:
|
|
133
|
+
# Some bind-mount backends (observed: Docker Desktop's virtiofs) reject chmod on
|
|
134
|
+
# socket special files outright with EINVAL. Not worth crashing the runner over.
|
|
135
|
+
logger.warning("Could not chmod %s to 0o666: %s", SOCK_PATH, exc)
|
|
136
|
+
logger.info("Runner %s listening on %s", RUNNER_ID, SOCK_PATH)
|
|
137
|
+
|
|
138
|
+
async with server:
|
|
139
|
+
await server.serve_forever()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defaultAction": "SCMP_ACT_ERRNO",
|
|
3
|
+
"architectures": [
|
|
4
|
+
"SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32",
|
|
5
|
+
"SCMP_ARCH_AARCH64", "SCMP_ARCH_ARM"
|
|
6
|
+
],
|
|
7
|
+
"syscalls": [
|
|
8
|
+
{
|
|
9
|
+
"names": [
|
|
10
|
+
"accept", "accept4", "access", "arch_prctl",
|
|
11
|
+
"bind", "brk",
|
|
12
|
+
"capget", "capset", "chdir", "chmod", "clock_getres", "clock_gettime",
|
|
13
|
+
"clock_nanosleep", "clone", "clone3", "close", "close_range", "connect",
|
|
14
|
+
"dup", "dup2", "dup3",
|
|
15
|
+
"epoll_create1", "epoll_ctl", "epoll_pwait", "epoll_wait", "eventfd2",
|
|
16
|
+
"execve", "exit", "exit_group",
|
|
17
|
+
"faccessat", "faccessat2", "fchdir", "fchmod", "fchmodat", "fcntl",
|
|
18
|
+
"fstat", "fstatfs", "futex",
|
|
19
|
+
"getcwd", "getdents64", "getegid", "geteuid", "getgid", "getpeername",
|
|
20
|
+
"getpid", "getppid", "getrandom", "getsockname", "getsockopt", "gettid",
|
|
21
|
+
"gettimeofday", "getuid",
|
|
22
|
+
"ioctl",
|
|
23
|
+
"listen", "lseek", "lstat",
|
|
24
|
+
"madvise", "mkdir", "mkdirat", "mmap", "mprotect", "mremap", "msync",
|
|
25
|
+
"munmap",
|
|
26
|
+
"nanosleep", "newfstatat",
|
|
27
|
+
"open", "openat",
|
|
28
|
+
"pipe", "pipe2", "poll", "ppoll", "prctl", "pread64", "prlimit64",
|
|
29
|
+
"pselect6", "pwrite64",
|
|
30
|
+
"read", "readlink", "readlinkat", "readv", "recvfrom", "recvmsg",
|
|
31
|
+
"restart_syscall", "rseq", "rt_sigaction", "rt_sigprocmask",
|
|
32
|
+
"rt_sigreturn", "rt_sigtimedwait",
|
|
33
|
+
"select", "sendmsg", "sendto", "set_robust_list", "set_tid_address",
|
|
34
|
+
"setgid", "setgroups", "setns", "setresgid", "setresuid", "setsockopt",
|
|
35
|
+
"setuid", "shutdown", "sigaltstack", "socket", "socketpair", "stat",
|
|
36
|
+
"statfs", "statx", "sysinfo",
|
|
37
|
+
"tgkill",
|
|
38
|
+
"uname", "unlink", "unlinkat", "unshare",
|
|
39
|
+
"wait4", "write", "writev"
|
|
40
|
+
],
|
|
41
|
+
"action": "SCMP_ACT_ALLOW"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|