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.
Files changed (52) hide show
  1. tradingkit/__init__.py +147 -0
  2. tradingkit/_docker/compiler/Dockerfile +6 -0
  3. tradingkit/_docker/compiler/compile.sh +19 -0
  4. tradingkit/_docker/runner/Dockerfile +7 -0
  5. tradingkit/_docker/runner/runner_worker.py +143 -0
  6. tradingkit/_docker/runner/seccomp.json +44 -0
  7. tradingkit/aggregation.py +329 -0
  8. tradingkit/backtest/__init__.py +4 -0
  9. tradingkit/backtest/result.py +100 -0
  10. tradingkit/backtest/runner.py +133 -0
  11. tradingkit/collector.py +572 -0
  12. tradingkit/context.py +38 -0
  13. tradingkit/core/__init__.py +0 -0
  14. tradingkit/core/clickhouse/__init__.py +136 -0
  15. tradingkit/core/clickhouse/_candles.py +234 -0
  16. tradingkit/core/clickhouse/_connections.py +90 -0
  17. tradingkit/core/clickhouse/_indicators_signals.py +161 -0
  18. tradingkit/core/clickhouse/_plugin_library.py +43 -0
  19. tradingkit/core/clickhouse/_sql.py +138 -0
  20. tradingkit/core/clickhouse/_unit_tables.py +254 -0
  21. tradingkit/core/live_feed.py +78 -0
  22. tradingkit/core/params.py +148 -0
  23. tradingkit/core/plugin_registry.py +36 -0
  24. tradingkit/core/resolver.py +361 -0
  25. tradingkit/core/script_ast.py +56 -0
  26. tradingkit/core/timeframe.py +74 -0
  27. tradingkit/executor/__init__.py +23 -0
  28. tradingkit/executor/_worker.py +103 -0
  29. tradingkit/executor/base.py +82 -0
  30. tradingkit/executor/cpp_pool.py +161 -0
  31. tradingkit/executor/launchers/__init__.py +25 -0
  32. tradingkit/executor/launchers/base.py +44 -0
  33. tradingkit/executor/launchers/docker_.py +212 -0
  34. tradingkit/executor/launchers/subprocess_.py +89 -0
  35. tradingkit/executor/local.py +63 -0
  36. tradingkit/executor/remote.py +166 -0
  37. tradingkit/executor/subprocess_.py +189 -0
  38. tradingkit/indicator.py +520 -0
  39. tradingkit/pipeline.py +252 -0
  40. tradingkit/py.typed +0 -0
  41. tradingkit/runner/__init__.py +0 -0
  42. tradingkit/runner/_safe_pickle.py +94 -0
  43. tradingkit/runner/server.py +286 -0
  44. tradingkit/schema.py +292 -0
  45. tradingkit/source.py +259 -0
  46. tradingkit/strategy.py +380 -0
  47. tradingkit_py-0.1.0.dist-info/METADATA +303 -0
  48. tradingkit_py-0.1.0.dist-info/RECORD +52 -0
  49. tradingkit_py-0.1.0.dist-info/WHEEL +5 -0
  50. tradingkit_py-0.1.0.dist-info/entry_points.txt +2 -0
  51. tradingkit_py-0.1.0.dist-info/licenses/LICENSE +21 -0
  52. 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,6 @@
1
+ FROM debian:12-slim
2
+ RUN apt-get update && apt-get install -y --no-install-recommends gcc g++ && rm -rf /var/lib/apt/lists/*
3
+ WORKDIR /build
4
+ COPY compile.sh /compile.sh
5
+ RUN chmod +x /compile.sh
6
+ ENTRYPOINT ["/compile.sh"]
@@ -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
+ }