onesecondtrader 0.40.0__py3-none-any.whl → 0.41.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.
- onesecondtrader/__init__.py +2 -0
- onesecondtrader/connectors/datafeeds/simulated.py +21 -6
- onesecondtrader/core/datafeeds/base.py +3 -0
- onesecondtrader/core/models/__init__.py +2 -0
- onesecondtrader/core/models/params.py +21 -0
- onesecondtrader/core/strategies/base.py +9 -3
- onesecondtrader/core/strategies/examples.py +15 -7
- onesecondtrader/dashboard/__init__.py +3 -0
- onesecondtrader/dashboard/app.py +1677 -0
- onesecondtrader/dashboard/registry.py +100 -0
- onesecondtrader/orchestrator/__init__.py +7 -0
- onesecondtrader/orchestrator/orchestrator.py +105 -0
- onesecondtrader/orchestrator/recorder.py +196 -0
- onesecondtrader/orchestrator/schema.sql +208 -0
- onesecondtrader/secmaster/schema.sql +48 -0
- onesecondtrader/secmaster/utils.py +90 -0
- {onesecondtrader-0.40.0.dist-info → onesecondtrader-0.41.0.dist-info}/METADATA +3 -1
- {onesecondtrader-0.40.0.dist-info → onesecondtrader-0.41.0.dist-info}/RECORD +20 -13
- {onesecondtrader-0.40.0.dist-info → onesecondtrader-0.41.0.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.40.0.dist-info → onesecondtrader-0.41.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
|
|
5
|
+
from onesecondtrader.core.strategies import StrategyBase
|
|
6
|
+
from onesecondtrader.core.brokers import BrokerBase
|
|
7
|
+
from onesecondtrader.core.datafeeds import DatafeedBase
|
|
8
|
+
from onesecondtrader.core.models import ParamSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_subclasses(base: type) -> dict[str, type]:
|
|
12
|
+
result = {}
|
|
13
|
+
for cls in base.__subclasses__():
|
|
14
|
+
if not cls.__name__.startswith("_"):
|
|
15
|
+
result[cls.__name__] = cls
|
|
16
|
+
result.update(_get_subclasses(cls))
|
|
17
|
+
return result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_strategies() -> dict[str, type[StrategyBase]]:
|
|
21
|
+
return _get_subclasses(StrategyBase)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_brokers() -> dict[str, type[BrokerBase]]:
|
|
25
|
+
return _get_subclasses(BrokerBase)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_datafeeds() -> dict[str, type[DatafeedBase]]:
|
|
29
|
+
return _get_subclasses(DatafeedBase)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_param_schema(params: dict[str, ParamSpec]) -> list[dict]:
|
|
33
|
+
schema = []
|
|
34
|
+
for name, spec in params.items():
|
|
35
|
+
param_info = {
|
|
36
|
+
"name": name,
|
|
37
|
+
"default": _serialize_value(spec.default),
|
|
38
|
+
"type": _get_type_name(spec.default),
|
|
39
|
+
}
|
|
40
|
+
if spec.min is not None:
|
|
41
|
+
param_info["min"] = spec.min
|
|
42
|
+
if spec.max is not None:
|
|
43
|
+
param_info["max"] = spec.max
|
|
44
|
+
if spec.step is not None:
|
|
45
|
+
param_info["step"] = spec.step
|
|
46
|
+
choices = spec.resolved_choices
|
|
47
|
+
if choices is not None:
|
|
48
|
+
param_info["choices"] = [_serialize_value(c) for c in choices] # type: ignore[assignment]
|
|
49
|
+
schema.append(param_info)
|
|
50
|
+
return schema
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _serialize_value(value) -> str | int | float | bool:
|
|
54
|
+
if isinstance(value, enum.Enum):
|
|
55
|
+
return value.name
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_type_name(value) -> str:
|
|
60
|
+
if isinstance(value, enum.Enum):
|
|
61
|
+
return "enum"
|
|
62
|
+
if isinstance(value, bool):
|
|
63
|
+
return "bool"
|
|
64
|
+
if isinstance(value, int):
|
|
65
|
+
return "int"
|
|
66
|
+
if isinstance(value, float):
|
|
67
|
+
return "float"
|
|
68
|
+
if isinstance(value, str):
|
|
69
|
+
return "str"
|
|
70
|
+
return "unknown"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_strategy_schema(name: str) -> dict | None:
|
|
74
|
+
cls = get_strategies().get(name)
|
|
75
|
+
if cls is None:
|
|
76
|
+
return None
|
|
77
|
+
return {
|
|
78
|
+
"name": name,
|
|
79
|
+
"parameters": get_param_schema(getattr(cls, "parameters", {})),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_broker_schema(name: str) -> dict | None:
|
|
84
|
+
cls = get_brokers().get(name)
|
|
85
|
+
if cls is None:
|
|
86
|
+
return None
|
|
87
|
+
return {
|
|
88
|
+
"name": name,
|
|
89
|
+
"parameters": get_param_schema(getattr(cls, "parameters", {})),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_datafeed_schema(name: str) -> dict | None:
|
|
94
|
+
cls = get_datafeeds().get(name)
|
|
95
|
+
if cls is None:
|
|
96
|
+
return None
|
|
97
|
+
return {
|
|
98
|
+
"name": name,
|
|
99
|
+
"parameters": get_param_schema(getattr(cls, "parameters", {})),
|
|
100
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import dotenv
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from onesecondtrader.core.brokers import BrokerBase
|
|
5
|
+
from onesecondtrader.core.datafeeds import DatafeedBase
|
|
6
|
+
from onesecondtrader.core.messaging import EventBus
|
|
7
|
+
from onesecondtrader.core.strategies import StrategyBase
|
|
8
|
+
from .recorder import RunRecorder
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Orchestrator:
|
|
12
|
+
db_path: str = "runs.db"
|
|
13
|
+
mode: str = "backtest"
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
strategies: list[type[StrategyBase]],
|
|
18
|
+
broker: type[BrokerBase],
|
|
19
|
+
datafeed: type[DatafeedBase],
|
|
20
|
+
) -> None:
|
|
21
|
+
dotenv.load_dotenv()
|
|
22
|
+
self._strategy_classes = strategies
|
|
23
|
+
self._broker_class = broker
|
|
24
|
+
self._datafeed_class = datafeed
|
|
25
|
+
self._event_bus: EventBus | None = None
|
|
26
|
+
self._strategies: list[StrategyBase] = []
|
|
27
|
+
self._broker: BrokerBase | None = None
|
|
28
|
+
self._datafeed: DatafeedBase | None = None
|
|
29
|
+
self._recorder: RunRecorder | None = None
|
|
30
|
+
|
|
31
|
+
def run(self) -> None:
|
|
32
|
+
run_id = self._generate_run_id()
|
|
33
|
+
symbols = self._collect_symbols()
|
|
34
|
+
bar_period = self._get_bar_period()
|
|
35
|
+
|
|
36
|
+
self._event_bus = EventBus()
|
|
37
|
+
|
|
38
|
+
self._recorder = self._create_recorder(run_id, symbols, bar_period)
|
|
39
|
+
self._broker = self._broker_class(self._event_bus)
|
|
40
|
+
self._strategies = [s(self._event_bus) for s in self._strategy_classes]
|
|
41
|
+
self._datafeed = self._datafeed_class(self._event_bus)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
self._broker.connect()
|
|
45
|
+
self._datafeed.connect()
|
|
46
|
+
self._subscribe_symbols()
|
|
47
|
+
self._datafeed.wait_until_complete()
|
|
48
|
+
self._event_bus.wait_until_system_idle()
|
|
49
|
+
finally:
|
|
50
|
+
self._shutdown()
|
|
51
|
+
|
|
52
|
+
def _generate_run_id(self) -> str:
|
|
53
|
+
timestamp = pd.Timestamp.now(tz="UTC").strftime("%Y-%m-%d_%H-%M-%S")
|
|
54
|
+
strategy_names = "_".join(s.__name__ for s in self._strategy_classes)
|
|
55
|
+
return f"{timestamp}_{strategy_names}"
|
|
56
|
+
|
|
57
|
+
def _collect_symbols(self) -> list[str]:
|
|
58
|
+
symbols = []
|
|
59
|
+
for strategy_class in self._strategy_classes:
|
|
60
|
+
symbols.extend(strategy_class.symbols)
|
|
61
|
+
return list(set(symbols))
|
|
62
|
+
|
|
63
|
+
def _get_bar_period(self) -> str | None:
|
|
64
|
+
if not self._strategy_classes:
|
|
65
|
+
return None
|
|
66
|
+
params = self._strategy_classes[0].parameters
|
|
67
|
+
if "bar_period" not in params:
|
|
68
|
+
return None
|
|
69
|
+
default = params["bar_period"].default
|
|
70
|
+
if hasattr(default, "name"):
|
|
71
|
+
return default.name # type: ignore[no-any-return]
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def _create_recorder(
|
|
75
|
+
self, run_id: str, symbols: list[str], bar_period: str | None
|
|
76
|
+
) -> RunRecorder:
|
|
77
|
+
class ConfiguredRecorder(RunRecorder):
|
|
78
|
+
db_path = self.db_path
|
|
79
|
+
|
|
80
|
+
assert self._event_bus is not None
|
|
81
|
+
return ConfiguredRecorder(
|
|
82
|
+
event_bus=self._event_bus,
|
|
83
|
+
run_id=run_id,
|
|
84
|
+
strategy="_".join(s.name for s in self._strategy_classes),
|
|
85
|
+
mode=self.mode,
|
|
86
|
+
symbols=symbols,
|
|
87
|
+
bar_period=bar_period,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _subscribe_symbols(self) -> None:
|
|
91
|
+
assert self._datafeed is not None
|
|
92
|
+
for strategy_class in self._strategy_classes:
|
|
93
|
+
bar_period = strategy_class.parameters["bar_period"].default
|
|
94
|
+
for symbol in strategy_class.symbols:
|
|
95
|
+
self._datafeed.subscribe(symbol, bar_period) # type: ignore[arg-type]
|
|
96
|
+
|
|
97
|
+
def _shutdown(self) -> None:
|
|
98
|
+
if self._datafeed:
|
|
99
|
+
self._datafeed.disconnect()
|
|
100
|
+
if self._broker:
|
|
101
|
+
self._broker.disconnect()
|
|
102
|
+
for strategy in self._strategies:
|
|
103
|
+
strategy.shutdown()
|
|
104
|
+
if self._recorder:
|
|
105
|
+
self._recorder.shutdown()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pathlib
|
|
3
|
+
import sqlite3
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from onesecondtrader.core import events
|
|
8
|
+
from onesecondtrader.core.messaging import EventBus, Subscriber
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RunRecorder(Subscriber):
|
|
12
|
+
db_path: str = "runs.db"
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
event_bus: EventBus,
|
|
17
|
+
run_id: str,
|
|
18
|
+
strategy: str,
|
|
19
|
+
mode: str,
|
|
20
|
+
symbols: list[str] | None = None,
|
|
21
|
+
bar_period: str | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._run_id = run_id
|
|
24
|
+
self._strategy = strategy
|
|
25
|
+
self._mode = mode
|
|
26
|
+
self._symbols = symbols or []
|
|
27
|
+
self._bar_period = bar_period
|
|
28
|
+
self._conn = self._init_db()
|
|
29
|
+
self._insert_run()
|
|
30
|
+
super().__init__(event_bus)
|
|
31
|
+
self._subscribe(
|
|
32
|
+
events.BarProcessed,
|
|
33
|
+
events.OrderSubmission,
|
|
34
|
+
events.OrderModification,
|
|
35
|
+
events.OrderCancellation,
|
|
36
|
+
events.OrderSubmissionAccepted,
|
|
37
|
+
events.OrderSubmissionRejected,
|
|
38
|
+
events.OrderModificationAccepted,
|
|
39
|
+
events.OrderModificationRejected,
|
|
40
|
+
events.OrderCancellationAccepted,
|
|
41
|
+
events.OrderCancellationRejected,
|
|
42
|
+
events.OrderFilled,
|
|
43
|
+
events.OrderExpired,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _init_db(self) -> sqlite3.Connection:
|
|
47
|
+
schema_path = pathlib.Path(__file__).parent / "schema.sql"
|
|
48
|
+
conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
49
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
50
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
51
|
+
conn.executescript(schema_path.read_text())
|
|
52
|
+
return conn
|
|
53
|
+
|
|
54
|
+
def _insert_run(self) -> None:
|
|
55
|
+
self._conn.execute(
|
|
56
|
+
"""
|
|
57
|
+
INSERT INTO runs (run_id, strategy, symbols, bar_period, mode, status, created_at)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
59
|
+
""",
|
|
60
|
+
(
|
|
61
|
+
self._run_id,
|
|
62
|
+
self._strategy,
|
|
63
|
+
json.dumps(self._symbols) if self._symbols else None,
|
|
64
|
+
self._bar_period,
|
|
65
|
+
self._mode,
|
|
66
|
+
"running",
|
|
67
|
+
pd.Timestamp.now(tz="UTC").isoformat(),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
self._conn.commit()
|
|
71
|
+
|
|
72
|
+
def _on_event(self, event: events.EventBase) -> None:
|
|
73
|
+
match event:
|
|
74
|
+
case events.BarProcessed():
|
|
75
|
+
self._record_bar(event)
|
|
76
|
+
case events.OrderSubmission():
|
|
77
|
+
self._record_order_request(event, "submission")
|
|
78
|
+
case events.OrderModification():
|
|
79
|
+
self._record_order_request(event, "modification")
|
|
80
|
+
case events.OrderCancellation():
|
|
81
|
+
self._record_order_request(event, "cancellation")
|
|
82
|
+
case events.OrderFilled():
|
|
83
|
+
self._record_fill(event)
|
|
84
|
+
case events.OrderSubmissionAccepted():
|
|
85
|
+
self._record_order_response(event, "submission_accepted")
|
|
86
|
+
case events.OrderSubmissionRejected():
|
|
87
|
+
self._record_order_response(event, "submission_rejected")
|
|
88
|
+
case events.OrderModificationAccepted():
|
|
89
|
+
self._record_order_response(event, "modification_accepted")
|
|
90
|
+
case events.OrderModificationRejected():
|
|
91
|
+
self._record_order_response(event, "modification_rejected")
|
|
92
|
+
case events.OrderCancellationAccepted():
|
|
93
|
+
self._record_order_response(event, "cancellation_accepted")
|
|
94
|
+
case events.OrderCancellationRejected():
|
|
95
|
+
self._record_order_response(event, "cancellation_rejected")
|
|
96
|
+
case events.OrderExpired():
|
|
97
|
+
self._record_order_response(event, "expired")
|
|
98
|
+
|
|
99
|
+
def _record_bar(self, event: events.BarProcessed) -> None:
|
|
100
|
+
self._conn.execute(
|
|
101
|
+
"""
|
|
102
|
+
INSERT INTO bars (run_id, ts_event, symbol, bar_period, open, high, low, close, volume, indicators)
|
|
103
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
104
|
+
""",
|
|
105
|
+
(
|
|
106
|
+
self._run_id,
|
|
107
|
+
event.ts_event.isoformat(),
|
|
108
|
+
event.symbol,
|
|
109
|
+
event.bar_period.value,
|
|
110
|
+
event.open,
|
|
111
|
+
event.high,
|
|
112
|
+
event.low,
|
|
113
|
+
event.close,
|
|
114
|
+
event.volume,
|
|
115
|
+
json.dumps(event.indicators) if event.indicators else None,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
self._conn.commit()
|
|
119
|
+
|
|
120
|
+
def _record_order_request(
|
|
121
|
+
self, event: events.BrokerRequestEvent, request_type: str
|
|
122
|
+
) -> None:
|
|
123
|
+
order_type = getattr(event, "order_type", None)
|
|
124
|
+
side = getattr(event, "side", None)
|
|
125
|
+
self._conn.execute(
|
|
126
|
+
"""
|
|
127
|
+
INSERT INTO order_requests (run_id, ts_event, request_type, order_id, symbol, order_type, side, quantity, limit_price, stop_price)
|
|
128
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
129
|
+
""",
|
|
130
|
+
(
|
|
131
|
+
self._run_id,
|
|
132
|
+
event.ts_event.isoformat(),
|
|
133
|
+
request_type,
|
|
134
|
+
str(event.system_order_id),
|
|
135
|
+
getattr(event, "symbol", None),
|
|
136
|
+
order_type.value if order_type else None,
|
|
137
|
+
side.value if side else None,
|
|
138
|
+
getattr(event, "quantity", None),
|
|
139
|
+
getattr(event, "limit_price", None),
|
|
140
|
+
getattr(event, "stop_price", None),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
self._conn.commit()
|
|
144
|
+
|
|
145
|
+
def _record_order_response(
|
|
146
|
+
self, event: events.BrokerResponseEvent, response_type: str
|
|
147
|
+
) -> None:
|
|
148
|
+
self._conn.execute(
|
|
149
|
+
"""
|
|
150
|
+
INSERT INTO order_responses (run_id, ts_event, ts_broker, response_type, order_id, broker_order_id, reason)
|
|
151
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
152
|
+
""",
|
|
153
|
+
(
|
|
154
|
+
self._run_id,
|
|
155
|
+
event.ts_event.isoformat(),
|
|
156
|
+
event.ts_broker.isoformat(),
|
|
157
|
+
response_type,
|
|
158
|
+
str(event.associated_order_id),
|
|
159
|
+
getattr(event, "broker_order_id", None),
|
|
160
|
+
getattr(event, "reason", None),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
self._conn.commit()
|
|
164
|
+
|
|
165
|
+
def _record_fill(self, event: events.OrderFilled) -> None:
|
|
166
|
+
self._conn.execute(
|
|
167
|
+
"""
|
|
168
|
+
INSERT INTO fills (run_id, ts_event, ts_broker, fill_id, order_id, broker_fill_id, symbol, side, quantity, price, commission, exchange)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
""",
|
|
171
|
+
(
|
|
172
|
+
self._run_id,
|
|
173
|
+
event.ts_event.isoformat(),
|
|
174
|
+
event.ts_broker.isoformat(),
|
|
175
|
+
str(event.fill_id),
|
|
176
|
+
str(event.associated_order_id),
|
|
177
|
+
event.broker_fill_id,
|
|
178
|
+
event.symbol,
|
|
179
|
+
event.side.value,
|
|
180
|
+
event.quantity_filled,
|
|
181
|
+
event.fill_price,
|
|
182
|
+
event.commission,
|
|
183
|
+
event.exchange,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
self._conn.commit()
|
|
187
|
+
|
|
188
|
+
def _cleanup(self) -> None:
|
|
189
|
+
self._conn.execute(
|
|
190
|
+
"""
|
|
191
|
+
UPDATE runs SET status = ?, completed_at = ? WHERE run_id = ?
|
|
192
|
+
""",
|
|
193
|
+
("completed", pd.Timestamp.now(tz="UTC").isoformat(), self._run_id),
|
|
194
|
+
)
|
|
195
|
+
self._conn.commit()
|
|
196
|
+
self._conn.close()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
-- Run Recorder Database Schema (runs.db)
|
|
2
|
+
--
|
|
3
|
+
-- A single database file storing all strategy runs. This schema captures the
|
|
4
|
+
-- complete lifecycle of each run including metadata, processed bars with
|
|
5
|
+
-- indicator values, order requests, broker responses, and fill executions.
|
|
6
|
+
--
|
|
7
|
+
-- The database uses WAL (Write-Ahead Logging) mode for concurrent read/write
|
|
8
|
+
-- access, allowing the dashboard to read while a strategy is actively writing.
|
|
9
|
+
--
|
|
10
|
+
-- All tables reference `run_id` to associate records with their parent run.
|
|
11
|
+
-- This enables efficient querying within a run and across multiple runs.
|
|
12
|
+
--
|
|
13
|
+
-- Tables:
|
|
14
|
+
--
|
|
15
|
+
-- | Table | Description |
|
|
16
|
+
-- |-------|-------------|
|
|
17
|
+
-- | `runs` | Run metadata including strategy name, symbols, status, and timestamps. |
|
|
18
|
+
-- | `bars` | Processed OHLCV bars with computed indicator values. |
|
|
19
|
+
-- | `order_requests` | Order requests from strategy to broker: submissions, modifications, cancellations. |
|
|
20
|
+
-- | `order_responses` | Broker responses: acceptances, rejections, expirations. |
|
|
21
|
+
-- | `fills` | Executed fills with price, quantity, commission, and exchange information. |
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
-- Run metadata capturing the configuration and status of strategy executions.
|
|
25
|
+
--
|
|
26
|
+
-- Each row represents a single strategy run. The table stores the initial
|
|
27
|
+
-- configuration used to start the run as well as runtime status that is updated
|
|
28
|
+
-- when the run completes or is interrupted.
|
|
29
|
+
--
|
|
30
|
+
-- | Field | Type | Constraints | Description |
|
|
31
|
+
-- |-------|------|-------------|-------------|
|
|
32
|
+
-- | `run_id` | TEXT | PRIMARY KEY | Unique identifier for this run, typically formatted as `YYYY-MM-DD_HH-MM-SS_StrategyName` |
|
|
33
|
+
-- | `strategy` | TEXT | NOT NULL | Name of the strategy class that was executed |
|
|
34
|
+
-- | `symbols` | TEXT | | JSON array of symbols traded during this run (e.g., `["AAPL", "MSFT"]`) |
|
|
35
|
+
-- | `bar_period` | TEXT | | Bar period used by the strategy (e.g., `SECOND`, `MINUTE`, `HOUR`, `DAY`) |
|
|
36
|
+
-- | `mode` | TEXT | NOT NULL | Execution mode: `backtest` for historical simulation or `live` for real-time trading |
|
|
37
|
+
-- | `status` | TEXT | NOT NULL | Current run status: `running`, `completed`, or `interrupted` |
|
|
38
|
+
-- | `created_at` | TEXT | NOT NULL | ISO 8601 timestamp when the run was started |
|
|
39
|
+
-- | `completed_at` | TEXT | | ISO 8601 timestamp when the run finished (NULL if still running or interrupted) |
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
42
|
+
run_id TEXT PRIMARY KEY,
|
|
43
|
+
strategy TEXT NOT NULL,
|
|
44
|
+
symbols TEXT,
|
|
45
|
+
bar_period TEXT,
|
|
46
|
+
mode TEXT NOT NULL,
|
|
47
|
+
status TEXT NOT NULL,
|
|
48
|
+
created_at TEXT NOT NULL,
|
|
49
|
+
completed_at TEXT
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
-- Processed OHLCV bars with indicator values computed by the strategy.
|
|
54
|
+
--
|
|
55
|
+
-- Each row represents a single bar that was processed by the strategy. The bar
|
|
56
|
+
-- data comes from `BarProcessed` events which include both the raw OHLCV values
|
|
57
|
+
-- and any indicator values computed by the strategy's indicators.
|
|
58
|
+
--
|
|
59
|
+
-- Indicator values are stored as a JSON object where keys are indicator names
|
|
60
|
+
-- prefixed with their plot position (e.g., `{"00_SMA_20": 150.5, "01_RSI": 65.2}`).
|
|
61
|
+
--
|
|
62
|
+
-- | Field | Type | Constraints | Description |
|
|
63
|
+
-- |-------|------|-------------|-------------|
|
|
64
|
+
-- | `id` | INTEGER | PRIMARY KEY | Auto-incrementing unique identifier for this bar record |
|
|
65
|
+
-- | `run_id` | TEXT | NOT NULL, FK | Reference to the parent run in the `runs` table |
|
|
66
|
+
-- | `ts_event` | TEXT | NOT NULL | ISO 8601 timestamp of the bar's closing time |
|
|
67
|
+
-- | `symbol` | TEXT | NOT NULL | Trading symbol (e.g., `AAPL`, `ESH5`) |
|
|
68
|
+
-- | `bar_period` | TEXT | NOT NULL | Bar aggregation period: `SECOND`, `MINUTE`, `HOUR`, or `DAY` |
|
|
69
|
+
-- | `open` | REAL | NOT NULL | Opening price of the bar |
|
|
70
|
+
-- | `high` | REAL | NOT NULL | Highest price during the bar |
|
|
71
|
+
-- | `low` | REAL | NOT NULL | Lowest price during the bar |
|
|
72
|
+
-- | `close` | REAL | NOT NULL | Closing price of the bar |
|
|
73
|
+
-- | `volume` | INTEGER | | Trading volume during the bar (NULL if not available) |
|
|
74
|
+
-- | `indicators` | TEXT | | JSON object of indicator name-value pairs computed for this bar |
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS bars (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
79
|
+
ts_event TEXT NOT NULL,
|
|
80
|
+
symbol TEXT NOT NULL,
|
|
81
|
+
bar_period TEXT NOT NULL,
|
|
82
|
+
open REAL NOT NULL,
|
|
83
|
+
high REAL NOT NULL,
|
|
84
|
+
low REAL NOT NULL,
|
|
85
|
+
close REAL NOT NULL,
|
|
86
|
+
volume INTEGER,
|
|
87
|
+
indicators TEXT
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
-- Order requests sent from the strategy to the broker.
|
|
92
|
+
--
|
|
93
|
+
-- Each row represents a request event initiated by the strategy. This includes
|
|
94
|
+
-- new order submissions, modifications to existing orders, and cancellation
|
|
95
|
+
-- requests. These are the "outbound" events from the strategy's perspective.
|
|
96
|
+
--
|
|
97
|
+
-- | Field | Type | Constraints | Description |
|
|
98
|
+
-- |-------|------|-------------|-------------|
|
|
99
|
+
-- | `id` | INTEGER | PRIMARY KEY | Auto-incrementing unique identifier for this request record |
|
|
100
|
+
-- | `run_id` | TEXT | NOT NULL, FK | Reference to the parent run in the `runs` table |
|
|
101
|
+
-- | `ts_event` | TEXT | NOT NULL | ISO 8601 timestamp when the request was sent |
|
|
102
|
+
-- | `request_type` | TEXT | NOT NULL | Request type: `submission`, `modification`, or `cancellation` |
|
|
103
|
+
-- | `order_id` | TEXT | NOT NULL | System-assigned UUID for this order |
|
|
104
|
+
-- | `symbol` | TEXT | NOT NULL | Trading symbol |
|
|
105
|
+
-- | `order_type` | TEXT | | Order type: `MARKET`, `LIMIT`, `STOP`, `STOP_LIMIT` (submission only) |
|
|
106
|
+
-- | `side` | TEXT | | Order side: `BUY` or `SELL` (submission only) |
|
|
107
|
+
-- | `quantity` | REAL | | Order quantity (submission and modification) |
|
|
108
|
+
-- | `limit_price` | REAL | | Limit price for LIMIT and STOP_LIMIT orders |
|
|
109
|
+
-- | `stop_price` | REAL | | Stop trigger price for STOP and STOP_LIMIT orders |
|
|
110
|
+
|
|
111
|
+
CREATE TABLE IF NOT EXISTS order_requests (
|
|
112
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
113
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
114
|
+
ts_event TEXT NOT NULL,
|
|
115
|
+
request_type TEXT NOT NULL,
|
|
116
|
+
order_id TEXT NOT NULL,
|
|
117
|
+
symbol TEXT NOT NULL,
|
|
118
|
+
order_type TEXT,
|
|
119
|
+
side TEXT,
|
|
120
|
+
quantity REAL,
|
|
121
|
+
limit_price REAL,
|
|
122
|
+
stop_price REAL
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
-- Broker responses to order requests.
|
|
127
|
+
--
|
|
128
|
+
-- Each row represents a response event from the broker. This includes acceptances,
|
|
129
|
+
-- rejections, and expirations. These are the "inbound" events from the broker's
|
|
130
|
+
-- perspective. Fills are stored separately in the `fills` table.
|
|
131
|
+
--
|
|
132
|
+
-- | Field | Type | Constraints | Description |
|
|
133
|
+
-- |-------|------|-------------|-------------|
|
|
134
|
+
-- | `id` | INTEGER | PRIMARY KEY | Auto-incrementing unique identifier for this response record |
|
|
135
|
+
-- | `run_id` | TEXT | NOT NULL, FK | Reference to the parent run in the `runs` table |
|
|
136
|
+
-- | `ts_event` | TEXT | NOT NULL | ISO 8601 timestamp when the response was received |
|
|
137
|
+
-- | `ts_broker` | TEXT | NOT NULL | ISO 8601 timestamp when the broker processed the request |
|
|
138
|
+
-- | `response_type` | TEXT | NOT NULL | Response type: `submission_accepted`, `submission_rejected`, `modification_accepted`, `modification_rejected`, `cancellation_accepted`, `cancellation_rejected`, `expired` |
|
|
139
|
+
-- | `order_id` | TEXT | NOT NULL | System-assigned UUID of the associated order |
|
|
140
|
+
-- | `broker_order_id` | TEXT | | Broker-assigned order identifier (on acceptance) |
|
|
141
|
+
-- | `reason` | TEXT | | Rejection reason (on rejection) |
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS order_responses (
|
|
144
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
145
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
146
|
+
ts_event TEXT NOT NULL,
|
|
147
|
+
ts_broker TEXT NOT NULL,
|
|
148
|
+
response_type TEXT NOT NULL,
|
|
149
|
+
order_id TEXT NOT NULL,
|
|
150
|
+
broker_order_id TEXT,
|
|
151
|
+
reason TEXT
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
-- Fill executions recording completed trades.
|
|
156
|
+
--
|
|
157
|
+
-- Each row represents a single fill event where an order was partially or fully
|
|
158
|
+
-- executed. An order may have multiple fills if it is executed in parts. Fills
|
|
159
|
+
-- are separated from other broker responses due to their distinct structure and
|
|
160
|
+
-- importance for P&L calculations.
|
|
161
|
+
--
|
|
162
|
+
-- | Field | Type | Constraints | Description |
|
|
163
|
+
-- |-------|------|-------------|-------------|
|
|
164
|
+
-- | `id` | INTEGER | PRIMARY KEY | Auto-incrementing unique identifier for this fill record |
|
|
165
|
+
-- | `run_id` | TEXT | NOT NULL, FK | Reference to the parent run in the `runs` table |
|
|
166
|
+
-- | `ts_event` | TEXT | NOT NULL | ISO 8601 timestamp when the fill event was received |
|
|
167
|
+
-- | `ts_broker` | TEXT | NOT NULL | ISO 8601 timestamp when the broker executed the fill |
|
|
168
|
+
-- | `fill_id` | TEXT | NOT NULL | System-assigned UUID for this specific fill |
|
|
169
|
+
-- | `order_id` | TEXT | NOT NULL | System-assigned UUID of the order that was filled |
|
|
170
|
+
-- | `broker_fill_id` | TEXT | | Broker-assigned fill identifier |
|
|
171
|
+
-- | `symbol` | TEXT | NOT NULL | Trading symbol that was filled |
|
|
172
|
+
-- | `side` | TEXT | NOT NULL | Fill side: `BUY` or `SELL` |
|
|
173
|
+
-- | `quantity` | REAL | NOT NULL | Number of units filled |
|
|
174
|
+
-- | `price` | REAL | NOT NULL | Execution price |
|
|
175
|
+
-- | `commission` | REAL | | Commission charged for this fill |
|
|
176
|
+
-- | `exchange` | TEXT | | Exchange or venue where the fill occurred |
|
|
177
|
+
|
|
178
|
+
CREATE TABLE IF NOT EXISTS fills (
|
|
179
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
180
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
181
|
+
ts_event TEXT NOT NULL,
|
|
182
|
+
ts_broker TEXT NOT NULL,
|
|
183
|
+
fill_id TEXT NOT NULL,
|
|
184
|
+
order_id TEXT NOT NULL,
|
|
185
|
+
broker_fill_id TEXT,
|
|
186
|
+
symbol TEXT NOT NULL,
|
|
187
|
+
side TEXT NOT NULL,
|
|
188
|
+
quantity REAL NOT NULL,
|
|
189
|
+
price REAL NOT NULL,
|
|
190
|
+
commission REAL,
|
|
191
|
+
exchange TEXT
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
-- Indexes for efficient querying by run, timestamp, and order relationships.
|
|
196
|
+
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_bars_run_id ON bars(run_id);
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_bars_run_ts ON bars(run_id, ts_event);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_bars_run_symbol ON bars(run_id, symbol);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_order_requests_run_id ON order_requests(run_id);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_order_requests_run_ts ON order_requests(run_id, ts_event);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_order_requests_order_id ON order_requests(order_id);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_order_responses_run_id ON order_responses(run_id);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_order_responses_run_ts ON order_responses(run_id, ts_event);
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_order_responses_order_id ON order_responses(order_id);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS idx_fills_run_id ON fills(run_id);
|
|
207
|
+
CREATE INDEX IF NOT EXISTS idx_fills_run_ts ON fills(run_id, ts_event);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_fills_order_id ON fills(order_id);
|
|
@@ -690,3 +690,51 @@ CREATE TABLE symbology (
|
|
|
690
690
|
end_date TEXT NOT NULL,
|
|
691
691
|
PRIMARY KEY (symbol, instrument_id, start_date)
|
|
692
692
|
);
|
|
693
|
+
|
|
694
|
+
-- Precomputed database metadata for fast dashboard queries.
|
|
695
|
+
--
|
|
696
|
+
-- This table stores aggregate statistics about the database contents that would
|
|
697
|
+
-- otherwise require expensive COUNT(*) queries on large tables. Stats are updated
|
|
698
|
+
-- after each data ingestion operation.
|
|
699
|
+
--
|
|
700
|
+
-- | Field | Type | Constraints | Description |
|
|
701
|
+
-- |-------|------|-------------|-------------|
|
|
702
|
+
-- | `key` | TEXT | PRIMARY KEY | The name of the statistic (e.g., 'symbol_count', 'ohlcv_record_count') |
|
|
703
|
+
-- | `value` | TEXT | NOT NULL | The value of the statistic, stored as text for flexibility |
|
|
704
|
+
CREATE TABLE meta (
|
|
705
|
+
key TEXT PRIMARY KEY,
|
|
706
|
+
value TEXT NOT NULL
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
-- Precomputed per-symbol coverage statistics for fast search queries.
|
|
710
|
+
--
|
|
711
|
+
-- This table stores aggregate statistics for each symbol/rtype combination,
|
|
712
|
+
-- eliminating the need for expensive JOIN and GROUP BY queries on the large
|
|
713
|
+
-- ohlcv table during symbol search. Stats are updated after each data ingestion.
|
|
714
|
+
--
|
|
715
|
+
-- | Field | Type | Constraints | Description |
|
|
716
|
+
-- |-------|------|-------------|-------------|
|
|
717
|
+
-- | `symbol` | TEXT | NOT NULL, PK | The ticker symbol |
|
|
718
|
+
-- | `rtype` | INTEGER | NOT NULL, PK | The bar duration type (32=1s, 33=1m, 34=1h, 35=1d) |
|
|
719
|
+
-- | `min_ts` | INTEGER | NOT NULL | The earliest timestamp for this symbol/rtype |
|
|
720
|
+
-- | `max_ts` | INTEGER | NOT NULL | The latest timestamp for this symbol/rtype |
|
|
721
|
+
-- | `record_count` | INTEGER | NOT NULL | Total number of records for this symbol/rtype |
|
|
722
|
+
CREATE TABLE symbol_coverage (
|
|
723
|
+
symbol TEXT NOT NULL,
|
|
724
|
+
rtype INTEGER NOT NULL,
|
|
725
|
+
min_ts INTEGER NOT NULL,
|
|
726
|
+
max_ts INTEGER NOT NULL,
|
|
727
|
+
record_count INTEGER NOT NULL,
|
|
728
|
+
PRIMARY KEY (symbol, rtype)
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
-- User-defined symbol presets for quick selection in the dashboard.
|
|
732
|
+
--
|
|
733
|
+
-- | Field | Type | Constraints | Description |
|
|
734
|
+
-- |-------|------|-------------|-------------|
|
|
735
|
+
-- | `name` | TEXT | PRIMARY KEY | Unique name for the preset (e.g., 'Tech Stocks', 'My Watchlist') |
|
|
736
|
+
-- | `symbols` | TEXT | NOT NULL | JSON array of symbol strings (e.g., '["AAPL", "GOOGL", "MSFT"]') |
|
|
737
|
+
CREATE TABLE symbol_presets (
|
|
738
|
+
name TEXT PRIMARY KEY,
|
|
739
|
+
symbols TEXT NOT NULL
|
|
740
|
+
);
|