onesecondtrader 0.40.0__py3-none-any.whl → 0.43.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 +4 -0
- onesecondtrader/connectors/datafeeds/simulated.py +63 -17
- onesecondtrader/core/datafeeds/base.py +3 -0
- onesecondtrader/core/events/requests.py +2 -0
- onesecondtrader/core/models/__init__.py +4 -1
- onesecondtrader/core/models/orders.py +12 -0
- onesecondtrader/core/models/params.py +21 -0
- onesecondtrader/core/models/records.py +2 -0
- onesecondtrader/core/strategies/base.py +17 -4
- onesecondtrader/core/strategies/examples.py +19 -7
- onesecondtrader/dashboard/__init__.py +3 -0
- onesecondtrader/dashboard/app.py +2972 -0
- onesecondtrader/dashboard/registry.py +100 -0
- onesecondtrader/orchestrator/__init__.py +7 -0
- onesecondtrader/orchestrator/orchestrator.py +105 -0
- onesecondtrader/orchestrator/recorder.py +199 -0
- onesecondtrader/orchestrator/schema.sql +212 -0
- onesecondtrader/secmaster/schema.sql +48 -0
- onesecondtrader/secmaster/utils.py +90 -0
- {onesecondtrader-0.40.0.dist-info → onesecondtrader-0.43.0.dist-info}/METADATA +3 -1
- {onesecondtrader-0.40.0.dist-info → onesecondtrader-0.43.0.dist-info}/RECORD +23 -16
- {onesecondtrader-0.40.0.dist-info → onesecondtrader-0.43.0.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.40.0.dist-info → onesecondtrader-0.43.0.dist-info}/licenses/LICENSE +0 -0
onesecondtrader/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
__all__ = [
|
|
2
|
+
"ActionType",
|
|
2
3
|
"BarPeriod",
|
|
3
4
|
"BarProcessed",
|
|
4
5
|
"BarReceived",
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
"OrderSide",
|
|
19
20
|
"OrderSubmission",
|
|
20
21
|
"OrderType",
|
|
22
|
+
"ParamSpec",
|
|
21
23
|
"SimulatedBroker",
|
|
22
24
|
"SimulatedDatafeed",
|
|
23
25
|
"SimpleMovingAverage",
|
|
@@ -46,11 +48,13 @@ from onesecondtrader.core.indicators import (
|
|
|
46
48
|
Volume,
|
|
47
49
|
)
|
|
48
50
|
from onesecondtrader.core.models import (
|
|
51
|
+
ActionType,
|
|
49
52
|
BarPeriod,
|
|
50
53
|
FillRecord,
|
|
51
54
|
InputSource,
|
|
52
55
|
OrderRecord,
|
|
53
56
|
OrderSide,
|
|
54
57
|
OrderType,
|
|
58
|
+
ParamSpec,
|
|
55
59
|
)
|
|
56
60
|
from onesecondtrader.core.strategies import SMACrossover, StrategyBase
|
|
@@ -21,6 +21,8 @@ _PRICE_SCALE = 1e9
|
|
|
21
21
|
|
|
22
22
|
class SimulatedDatafeed(DatafeedBase):
|
|
23
23
|
db_path: str = ""
|
|
24
|
+
start_ts: int | None = None
|
|
25
|
+
end_ts: int | None = None
|
|
24
26
|
|
|
25
27
|
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
26
28
|
super().__init__(event_bus)
|
|
@@ -51,9 +53,14 @@ class SimulatedDatafeed(DatafeedBase):
|
|
|
51
53
|
self._connected = False
|
|
52
54
|
|
|
53
55
|
def subscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
54
|
-
if (symbol, bar_period) in self._subscriptions:
|
|
55
|
-
return
|
|
56
56
|
self._subscriptions.add((symbol, bar_period))
|
|
57
|
+
|
|
58
|
+
def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
59
|
+
self._subscriptions.discard((symbol, bar_period))
|
|
60
|
+
|
|
61
|
+
def wait_until_complete(self) -> None:
|
|
62
|
+
if not self._subscriptions:
|
|
63
|
+
return
|
|
57
64
|
if self._thread is None or not self._thread.is_alive():
|
|
58
65
|
self._stop_event.clear()
|
|
59
66
|
self._thread = threading.Thread(
|
|
@@ -62,9 +69,7 @@ class SimulatedDatafeed(DatafeedBase):
|
|
|
62
69
|
daemon=False,
|
|
63
70
|
)
|
|
64
71
|
self._thread.start()
|
|
65
|
-
|
|
66
|
-
def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
67
|
-
self._subscriptions.discard((symbol, bar_period))
|
|
72
|
+
self._thread.join()
|
|
68
73
|
|
|
69
74
|
def _stream(self) -> None:
|
|
70
75
|
if not self._connection:
|
|
@@ -83,26 +88,40 @@ class SimulatedDatafeed(DatafeedBase):
|
|
|
83
88
|
|
|
84
89
|
cursor = self._connection.cursor()
|
|
85
90
|
|
|
86
|
-
|
|
87
|
-
if not
|
|
91
|
+
symbology_map = self._load_symbology(cursor, symbols)
|
|
92
|
+
if not symbology_map:
|
|
88
93
|
return
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
all_instrument_ids = set()
|
|
96
|
+
for entries in symbology_map.values():
|
|
97
|
+
for entry in entries:
|
|
98
|
+
all_instrument_ids.add(entry["instrument_id"])
|
|
99
|
+
|
|
100
|
+
id_list = list(all_instrument_ids)
|
|
92
101
|
rtype_list = list(set(rtype_by_symbol.values()))
|
|
93
102
|
|
|
94
103
|
placeholders_ids = ",".join("?" * len(id_list))
|
|
95
104
|
placeholders_rtypes = ",".join("?" * len(rtype_list))
|
|
96
105
|
|
|
106
|
+
date_filter = ""
|
|
107
|
+
params = id_list + rtype_list
|
|
108
|
+
if self.start_ts is not None:
|
|
109
|
+
date_filter += " AND ts_event >= ?"
|
|
110
|
+
params.append(self.start_ts)
|
|
111
|
+
if self.end_ts is not None:
|
|
112
|
+
date_filter += " AND ts_event <= ?"
|
|
113
|
+
params.append(self.end_ts)
|
|
114
|
+
|
|
97
115
|
query = f"""
|
|
98
116
|
SELECT instrument_id, rtype, ts_event, open, high, low, close, volume
|
|
99
117
|
FROM ohlcv
|
|
100
118
|
WHERE instrument_id IN ({placeholders_ids})
|
|
101
119
|
AND rtype IN ({placeholders_rtypes})
|
|
120
|
+
{date_filter}
|
|
102
121
|
ORDER BY ts_event
|
|
103
122
|
"""
|
|
104
123
|
|
|
105
|
-
cursor.execute(query,
|
|
124
|
+
cursor.execute(query, params)
|
|
106
125
|
|
|
107
126
|
while True:
|
|
108
127
|
if self._stop_event.is_set():
|
|
@@ -113,7 +132,10 @@ class SimulatedDatafeed(DatafeedBase):
|
|
|
113
132
|
break
|
|
114
133
|
|
|
115
134
|
instrument_id, rtype, ts_event, open_, high, low, close, volume = row
|
|
116
|
-
|
|
135
|
+
|
|
136
|
+
symbol = self._resolve_symbol_for_bar(
|
|
137
|
+
symbology_map, instrument_id, ts_event
|
|
138
|
+
)
|
|
117
139
|
if symbol is None:
|
|
118
140
|
continue
|
|
119
141
|
|
|
@@ -137,16 +159,40 @@ class SimulatedDatafeed(DatafeedBase):
|
|
|
137
159
|
)
|
|
138
160
|
self._event_bus.wait_until_system_idle()
|
|
139
161
|
|
|
140
|
-
def
|
|
162
|
+
def _load_symbology(
|
|
141
163
|
self, cursor: sqlite3.Cursor, symbols: list[str]
|
|
142
|
-
) -> dict[str,
|
|
164
|
+
) -> dict[str, list[dict]]:
|
|
143
165
|
placeholders = ",".join("?" * len(symbols))
|
|
144
166
|
query = f"""
|
|
145
|
-
SELECT symbol, instrument_id
|
|
167
|
+
SELECT symbol, instrument_id, start_date, end_date
|
|
146
168
|
FROM symbology
|
|
147
169
|
WHERE symbol IN ({placeholders})
|
|
148
|
-
|
|
149
|
-
HAVING start_date = MAX(start_date)
|
|
170
|
+
ORDER BY symbol, start_date
|
|
150
171
|
"""
|
|
151
172
|
cursor.execute(query, symbols)
|
|
152
|
-
|
|
173
|
+
|
|
174
|
+
result: dict[str, list[dict]] = {}
|
|
175
|
+
for row in cursor.fetchall():
|
|
176
|
+
symbol, instrument_id, start_date, end_date = row
|
|
177
|
+
start_ns = int(pd.Timestamp(start_date, tz="UTC").value)
|
|
178
|
+
end_ns = int(pd.Timestamp(end_date, tz="UTC").value)
|
|
179
|
+
if symbol not in result:
|
|
180
|
+
result[symbol] = []
|
|
181
|
+
result[symbol].append(
|
|
182
|
+
{
|
|
183
|
+
"instrument_id": instrument_id,
|
|
184
|
+
"start_ns": start_ns,
|
|
185
|
+
"end_ns": end_ns,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def _resolve_symbol_for_bar(
|
|
191
|
+
self, symbology_map: dict[str, list[dict]], instrument_id: int, ts_event: int
|
|
192
|
+
) -> str | None:
|
|
193
|
+
for symbol, entries in symbology_map.items():
|
|
194
|
+
for entry in entries:
|
|
195
|
+
if entry["instrument_id"] == instrument_id:
|
|
196
|
+
if entry["start_ns"] <= ts_event < entry["end_ns"]:
|
|
197
|
+
return symbol
|
|
198
|
+
return None
|
|
@@ -16,6 +16,8 @@ class OrderSubmission(bases.BrokerRequestEvent):
|
|
|
16
16
|
quantity: float
|
|
17
17
|
limit_price: float | None = None
|
|
18
18
|
stop_price: float | None = None
|
|
19
|
+
action: models.orders.ActionType | None = None
|
|
20
|
+
signal: str | None = None
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
__all__ = [
|
|
2
|
+
"ActionType",
|
|
2
3
|
"BarPeriod",
|
|
3
4
|
"InputSource",
|
|
4
5
|
"OrderSide",
|
|
5
6
|
"OrderType",
|
|
6
7
|
"OrderRecord",
|
|
7
8
|
"FillRecord",
|
|
9
|
+
"ParamSpec",
|
|
8
10
|
]
|
|
9
11
|
|
|
10
12
|
from .data import BarPeriod, InputSource
|
|
11
|
-
from .orders import OrderSide, OrderType
|
|
13
|
+
from .orders import ActionType, OrderSide, OrderType
|
|
12
14
|
from .records import OrderRecord, FillRecord
|
|
15
|
+
from .params import ParamSpec
|
|
@@ -13,3 +13,15 @@ class OrderType(enum.Enum):
|
|
|
13
13
|
class OrderSide(enum.Enum):
|
|
14
14
|
BUY = enum.auto()
|
|
15
15
|
SELL = enum.auto()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ActionType(enum.Enum):
|
|
19
|
+
ENTRY = enum.auto()
|
|
20
|
+
ENTRY_LONG = enum.auto()
|
|
21
|
+
ENTRY_SHORT = enum.auto()
|
|
22
|
+
EXIT = enum.auto()
|
|
23
|
+
EXIT_LONG = enum.auto()
|
|
24
|
+
EXIT_SHORT = enum.auto()
|
|
25
|
+
ADD = enum.auto()
|
|
26
|
+
REDUCE = enum.auto()
|
|
27
|
+
REVERSE = enum.auto()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclasses.dataclass
|
|
8
|
+
class ParamSpec:
|
|
9
|
+
default: int | float | str | bool | enum.Enum
|
|
10
|
+
min: int | float | None = None
|
|
11
|
+
max: int | float | None = None
|
|
12
|
+
step: int | float | None = None
|
|
13
|
+
choices: list | None = None
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def resolved_choices(self) -> list | None:
|
|
17
|
+
if self.choices is not None:
|
|
18
|
+
return self.choices
|
|
19
|
+
if isinstance(self.default, enum.Enum):
|
|
20
|
+
return list(type(self.default))
|
|
21
|
+
return None
|
|
@@ -10,11 +10,17 @@ from onesecondtrader.core import events, indicators, messaging, models
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
13
|
+
name: str = ""
|
|
13
14
|
symbols: list[str] = []
|
|
14
|
-
|
|
15
|
+
parameters: dict[str, models.ParamSpec] = {}
|
|
15
16
|
|
|
16
|
-
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
17
|
+
def __init__(self, event_bus: messaging.EventBus, **overrides) -> None:
|
|
17
18
|
super().__init__(event_bus)
|
|
19
|
+
|
|
20
|
+
for name, spec in self.parameters.items():
|
|
21
|
+
value = overrides.get(name, spec.default)
|
|
22
|
+
setattr(self, name, value)
|
|
23
|
+
|
|
18
24
|
self._subscribe(
|
|
19
25
|
events.BarReceived,
|
|
20
26
|
events.OrderSubmissionAccepted,
|
|
@@ -70,8 +76,9 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
|
70
76
|
quantity: float,
|
|
71
77
|
limit_price: float | None = None,
|
|
72
78
|
stop_price: float | None = None,
|
|
79
|
+
action: models.ActionType | None = None,
|
|
80
|
+
signal: str | None = None,
|
|
73
81
|
) -> uuid.UUID:
|
|
74
|
-
# Uses bar timestamp for backtest compatibility; ts_created tracks real wall-clock time
|
|
75
82
|
order_id = uuid.uuid4()
|
|
76
83
|
|
|
77
84
|
event = events.OrderSubmission(
|
|
@@ -83,6 +90,8 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
|
83
90
|
quantity=quantity,
|
|
84
91
|
limit_price=limit_price,
|
|
85
92
|
stop_price=stop_price,
|
|
93
|
+
action=action,
|
|
94
|
+
signal=signal,
|
|
86
95
|
)
|
|
87
96
|
|
|
88
97
|
order = models.OrderRecord(
|
|
@@ -93,6 +102,8 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
|
93
102
|
quantity=quantity,
|
|
94
103
|
limit_price=limit_price,
|
|
95
104
|
stop_price=stop_price,
|
|
105
|
+
action=action,
|
|
106
|
+
signal=signal,
|
|
96
107
|
)
|
|
97
108
|
|
|
98
109
|
self._submitted_orders[order_id] = order
|
|
@@ -131,6 +142,8 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
|
131
142
|
stop_price=(
|
|
132
143
|
stop_price if stop_price is not None else original_order.stop_price
|
|
133
144
|
),
|
|
145
|
+
action=original_order.action,
|
|
146
|
+
signal=original_order.signal,
|
|
134
147
|
filled_quantity=original_order.filled_quantity,
|
|
135
148
|
)
|
|
136
149
|
|
|
@@ -179,7 +192,7 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
|
179
192
|
def _on_bar_received(self, event: events.BarReceived) -> None:
|
|
180
193
|
if event.symbol not in self.symbols:
|
|
181
194
|
return
|
|
182
|
-
if event.bar_period != self.bar_period:
|
|
195
|
+
if event.bar_period != self.bar_period: # type: ignore[attr-defined]
|
|
183
196
|
return
|
|
184
197
|
|
|
185
198
|
self._current_symbol = event.symbol
|
|
@@ -3,16 +3,20 @@ from .base import StrategyBase
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class SMACrossover(StrategyBase):
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
name = "SMA Crossover"
|
|
7
|
+
parameters = {
|
|
8
|
+
"bar_period": models.ParamSpec(default=models.BarPeriod.SECOND),
|
|
9
|
+
"fast_period": models.ParamSpec(default=20, min=5, max=100, step=1),
|
|
10
|
+
"slow_period": models.ParamSpec(default=100, min=10, max=500, step=1),
|
|
11
|
+
"quantity": models.ParamSpec(default=1.0, min=0.1, max=100.0, step=0.1),
|
|
12
|
+
}
|
|
9
13
|
|
|
10
14
|
def setup(self) -> None:
|
|
11
15
|
self.fast_sma = self.add_indicator(
|
|
12
|
-
indicators.SimpleMovingAverage(period=self.fast_period)
|
|
16
|
+
indicators.SimpleMovingAverage(period=self.fast_period) # type: ignore[attr-defined]
|
|
13
17
|
)
|
|
14
18
|
self.slow_sma = self.add_indicator(
|
|
15
|
-
indicators.SimpleMovingAverage(period=self.slow_period)
|
|
19
|
+
indicators.SimpleMovingAverage(period=self.slow_period) # type: ignore[attr-defined]
|
|
16
20
|
)
|
|
17
21
|
|
|
18
22
|
def on_bar(self, event: events.BarReceived) -> None:
|
|
@@ -22,7 +26,11 @@ class SMACrossover(StrategyBase):
|
|
|
22
26
|
and self.position <= 0
|
|
23
27
|
):
|
|
24
28
|
self.submit_order(
|
|
25
|
-
models.OrderType.MARKET,
|
|
29
|
+
models.OrderType.MARKET,
|
|
30
|
+
models.OrderSide.BUY,
|
|
31
|
+
self.quantity, # type: ignore[attr-defined]
|
|
32
|
+
action=models.ActionType.ENTRY,
|
|
33
|
+
signal="sma_crossover_up",
|
|
26
34
|
)
|
|
27
35
|
|
|
28
36
|
if (
|
|
@@ -31,5 +39,9 @@ class SMACrossover(StrategyBase):
|
|
|
31
39
|
and self.position >= 0
|
|
32
40
|
):
|
|
33
41
|
self.submit_order(
|
|
34
|
-
models.OrderType.MARKET,
|
|
42
|
+
models.OrderType.MARKET,
|
|
43
|
+
models.OrderSide.SELL,
|
|
44
|
+
self.quantity, # type: ignore[attr-defined]
|
|
45
|
+
action=models.ActionType.EXIT,
|
|
46
|
+
signal="sma_crossover_down",
|
|
35
47
|
)
|