onesecondtrader 0.22.0__py3-none-any.whl → 0.49.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/events/__init__.py +8 -0
- onesecondtrader/events/base.py +21 -0
- onesecondtrader/events/market/__init__.py +7 -0
- onesecondtrader/events/market/bar_processed.py +29 -0
- onesecondtrader/events/market/bar_received.py +34 -0
- onesecondtrader/events/orders/__init__.py +9 -0
- onesecondtrader/events/orders/base.py +30 -0
- onesecondtrader/events/orders/expirations.py +23 -0
- onesecondtrader/events/orders/fills.py +41 -0
- onesecondtrader/events/requests/__init__.py +11 -0
- onesecondtrader/events/requests/base.py +25 -0
- onesecondtrader/events/requests/order_cancellation.py +21 -0
- onesecondtrader/events/requests/order_modification.py +26 -0
- onesecondtrader/events/requests/order_submission.py +35 -0
- onesecondtrader/events/responses/__init__.py +14 -0
- onesecondtrader/events/responses/base.py +26 -0
- onesecondtrader/events/responses/cancellations.py +42 -0
- onesecondtrader/events/responses/modifications.py +43 -0
- onesecondtrader/events/responses/orders.py +42 -0
- onesecondtrader/indicators/__init__.py +17 -0
- onesecondtrader/indicators/base.py +142 -0
- onesecondtrader/indicators/market_fields.py +166 -0
- onesecondtrader/indicators/moving_averages.py +104 -0
- onesecondtrader/models/__init__.py +23 -0
- onesecondtrader/models/bar_fields.py +23 -0
- onesecondtrader/models/bar_period.py +21 -0
- onesecondtrader/models/order_types.py +21 -0
- onesecondtrader/models/rejection_reasons.py +48 -0
- onesecondtrader/models/trade_sides.py +20 -0
- {onesecondtrader-0.22.0.dist-info → onesecondtrader-0.49.0.dist-info}/METADATA +8 -2
- onesecondtrader-0.49.0.dist-info/RECORD +34 -0
- {onesecondtrader-0.22.0.dist-info → onesecondtrader-0.49.0.dist-info}/WHEEL +1 -1
- onesecondtrader/brokers.py +0 -92
- onesecondtrader/core.py +0 -260
- onesecondtrader/datafeeds.py +0 -173
- onesecondtrader/indicators.py +0 -106
- onesecondtrader-0.22.0.dist-info/RECORD +0 -9
- {onesecondtrader-0.22.0.dist-info → onesecondtrader-0.49.0.dist-info}/licenses/LICENSE +0 -0
onesecondtrader/brokers.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import uuid
|
|
3
|
-
|
|
4
|
-
from onesecondtrader.core import BaseConsumer, Events, event_bus
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class BaseBroker(BaseConsumer):
|
|
8
|
-
"""
|
|
9
|
-
Base class for all brokers.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
def __init__(self) -> None:
|
|
13
|
-
super().__init__()
|
|
14
|
-
event_bus.subscribe(self, Events.SubmitOrder)
|
|
15
|
-
event_bus.subscribe(self, Events.CancelOrder)
|
|
16
|
-
event_bus.subscribe(self, Events.ModifyOrder)
|
|
17
|
-
|
|
18
|
-
def on_event(self, event) -> None:
|
|
19
|
-
match event:
|
|
20
|
-
case Events.SubmitOrder():
|
|
21
|
-
self.on_submit_order(event)
|
|
22
|
-
case Events.CancelOrder():
|
|
23
|
-
self.on_cancel_order(event)
|
|
24
|
-
case Events.ModifyOrder():
|
|
25
|
-
self.on_modify_order(event)
|
|
26
|
-
|
|
27
|
-
@abc.abstractmethod
|
|
28
|
-
def on_submit_order(self, event: Events.SubmitOrder) -> None:
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
@abc.abstractmethod
|
|
32
|
-
def on_cancel_order(self, event: Events.CancelOrder) -> None:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
@abc.abstractmethod
|
|
36
|
-
def on_modify_order(self, event: Events.ModifyOrder) -> None:
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class SimulatedBroker(BaseBroker):
|
|
41
|
-
"""
|
|
42
|
-
Simulated broker for backtesting.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
def __init__(self) -> None:
|
|
46
|
-
super().__init__()
|
|
47
|
-
event_bus.subscribe(self, Events.IncomingBar)
|
|
48
|
-
|
|
49
|
-
self._pending_market_orders: dict[str, dict[uuid.UUID, Events.SubmitOrder]] = {}
|
|
50
|
-
self._pending_limit_orders: dict[str, dict[uuid.UUID, Events.SubmitOrder]] = {}
|
|
51
|
-
self._pending_stop_orders: dict[str, dict[uuid.UUID, Events.SubmitOrder]] = {}
|
|
52
|
-
self._pending_stop_limit_orders: dict[
|
|
53
|
-
str, dict[uuid.UUID, Events.SubmitOrder]
|
|
54
|
-
] = {}
|
|
55
|
-
|
|
56
|
-
def on_event(self, event) -> None:
|
|
57
|
-
match event:
|
|
58
|
-
case Events.SubmitOrder():
|
|
59
|
-
self.on_submit_order(event)
|
|
60
|
-
case Events.CancelOrder():
|
|
61
|
-
self.on_cancel_order(event)
|
|
62
|
-
case Events.ModifyOrder():
|
|
63
|
-
self.on_modify_order(event)
|
|
64
|
-
case Events.IncomingBar():
|
|
65
|
-
self.on_incoming_bar(event)
|
|
66
|
-
|
|
67
|
-
def on_submit_order(self, event: Events.SubmitOrder) -> None:
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
def on_cancel_order(self, event: Events.CancelOrder) -> None:
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
def on_modify_order(self, event: Events.ModifyOrder) -> None:
|
|
74
|
-
pass
|
|
75
|
-
|
|
76
|
-
def on_incoming_bar(self, event: Events.IncomingBar) -> None:
|
|
77
|
-
self._process_pending_orders(event)
|
|
78
|
-
|
|
79
|
-
bar_ready = Events.BarReady(
|
|
80
|
-
ts_event=event.ts_event,
|
|
81
|
-
symbol=event.symbol,
|
|
82
|
-
record_type=event.record_type,
|
|
83
|
-
open=event.open,
|
|
84
|
-
high=event.high,
|
|
85
|
-
low=event.low,
|
|
86
|
-
close=event.close,
|
|
87
|
-
volume=event.volume,
|
|
88
|
-
)
|
|
89
|
-
event_bus.publish(bar_ready)
|
|
90
|
-
|
|
91
|
-
def _process_pending_orders(self, event: Events.IncomingBar) -> None:
|
|
92
|
-
pass
|
onesecondtrader/core.py
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Core module containing the backbone of OneSecondTrader's event-driven architecture.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import abc
|
|
6
|
-
import dataclasses
|
|
7
|
-
import enum
|
|
8
|
-
import logging
|
|
9
|
-
import pandas as pd
|
|
10
|
-
import queue
|
|
11
|
-
import threading
|
|
12
|
-
import uuid
|
|
13
|
-
|
|
14
|
-
from collections import defaultdict
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
logging.basicConfig(
|
|
18
|
-
level=logging.DEBUG,
|
|
19
|
-
format="%(asctime)s - %(levelname)s - %(threadName)s - %(message)s",
|
|
20
|
-
)
|
|
21
|
-
logger = logging.getLogger("onesecondtrader")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class Models:
|
|
25
|
-
"""
|
|
26
|
-
Namespace for all models.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
class RecordType(enum.Enum):
|
|
30
|
-
OHLCV_1S = 32
|
|
31
|
-
OHLCV_1M = 33
|
|
32
|
-
OHLCV_1H = 34
|
|
33
|
-
OHLCV_1D = 35
|
|
34
|
-
|
|
35
|
-
class OrderSide(enum.Enum):
|
|
36
|
-
BUY = enum.auto()
|
|
37
|
-
SELL = enum.auto()
|
|
38
|
-
|
|
39
|
-
class OrderType(enum.Enum):
|
|
40
|
-
MARKET = enum.auto()
|
|
41
|
-
LIMIT = enum.auto()
|
|
42
|
-
STOP = enum.auto()
|
|
43
|
-
STOP_LIMIT = enum.auto()
|
|
44
|
-
|
|
45
|
-
class RejectionReason(enum.Enum):
|
|
46
|
-
ORDER_ALREADY_FILLED = enum.auto()
|
|
47
|
-
ORDER_ALREADY_CANCELLED = enum.auto()
|
|
48
|
-
ORDER_PENDING_EXECUTION = enum.auto()
|
|
49
|
-
INSUFFICIENT_FUNDS = enum.auto()
|
|
50
|
-
MARKET_CLOSED = enum.auto()
|
|
51
|
-
UNKNOWN = enum.auto()
|
|
52
|
-
|
|
53
|
-
class TimeInForce(enum.Enum):
|
|
54
|
-
GTC = enum.auto()
|
|
55
|
-
DAY = enum.auto()
|
|
56
|
-
IOC = enum.auto()
|
|
57
|
-
FOK = enum.auto()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class Events:
|
|
61
|
-
"""
|
|
62
|
-
Namespace for all events.
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
# BASE EVENT
|
|
66
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
67
|
-
class BaseEvent:
|
|
68
|
-
ts_event: pd.Timestamp = dataclasses.field(
|
|
69
|
-
default_factory=lambda: pd.Timestamp.now(tz="UTC")
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# SYSTEM EVENTS
|
|
73
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
74
|
-
class SystemEvent(BaseEvent):
|
|
75
|
-
pass
|
|
76
|
-
|
|
77
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
78
|
-
class SystemShutdown(SystemEvent):
|
|
79
|
-
pass
|
|
80
|
-
|
|
81
|
-
# MARKET EVENTS
|
|
82
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
83
|
-
class MarketEvent(BaseEvent):
|
|
84
|
-
pass
|
|
85
|
-
|
|
86
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
87
|
-
class IncomingBar(MarketEvent):
|
|
88
|
-
ts_event: pd.Timestamp
|
|
89
|
-
symbol: str
|
|
90
|
-
record_type: Models.RecordType
|
|
91
|
-
open: float
|
|
92
|
-
high: float
|
|
93
|
-
low: float
|
|
94
|
-
close: float
|
|
95
|
-
volume: int | None = None
|
|
96
|
-
|
|
97
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
98
|
-
class BarReady(MarketEvent):
|
|
99
|
-
ts_event: pd.Timestamp
|
|
100
|
-
symbol: str
|
|
101
|
-
record_type: Models.RecordType
|
|
102
|
-
open: float
|
|
103
|
-
high: float
|
|
104
|
-
low: float
|
|
105
|
-
close: float
|
|
106
|
-
volume: int | None = None
|
|
107
|
-
|
|
108
|
-
# BROKER REQUESTS EVENTS
|
|
109
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
110
|
-
class BrokerRequestEvent(BaseEvent):
|
|
111
|
-
pass
|
|
112
|
-
|
|
113
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
114
|
-
class SubmitOrder(BrokerRequestEvent):
|
|
115
|
-
order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
116
|
-
symbol: str
|
|
117
|
-
order_type: Models.OrderType
|
|
118
|
-
side: Models.OrderSide
|
|
119
|
-
quantity: float
|
|
120
|
-
limit_price: float | None = None
|
|
121
|
-
stop_price: float | None = None
|
|
122
|
-
time_in_force: Models.TimeInForce = Models.TimeInForce.GTC
|
|
123
|
-
|
|
124
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
125
|
-
class ModifyOrder(BrokerRequestEvent):
|
|
126
|
-
symbol: str
|
|
127
|
-
order_id: uuid.UUID
|
|
128
|
-
quantity: float | None = None
|
|
129
|
-
limit_price: float | None = None
|
|
130
|
-
stop_price: float | None = None
|
|
131
|
-
|
|
132
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
133
|
-
class CancelOrder(BrokerRequestEvent):
|
|
134
|
-
symbol: str
|
|
135
|
-
order_id: uuid.UUID
|
|
136
|
-
|
|
137
|
-
# BROKER RESPONSE EVENTS
|
|
138
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
139
|
-
class BrokerResponseEvent(BaseEvent):
|
|
140
|
-
ts_broker: pd.Timestamp
|
|
141
|
-
|
|
142
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
143
|
-
class OrderSubmitted(BrokerResponseEvent):
|
|
144
|
-
order_id: uuid.UUID
|
|
145
|
-
broker_order_id: str | None = None
|
|
146
|
-
|
|
147
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
148
|
-
class OrderModified(BrokerResponseEvent):
|
|
149
|
-
order_id: uuid.UUID
|
|
150
|
-
broker_order_id: str | None = None
|
|
151
|
-
|
|
152
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
153
|
-
class Fill(BrokerResponseEvent):
|
|
154
|
-
fill_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
155
|
-
broker_fill_id: str | None = None
|
|
156
|
-
associated_order_id: uuid.UUID
|
|
157
|
-
symbol: str
|
|
158
|
-
side: Models.OrderSide
|
|
159
|
-
quantity_filled: float
|
|
160
|
-
fill_price: float
|
|
161
|
-
commission: float
|
|
162
|
-
exchange: str = "SIMULATED"
|
|
163
|
-
|
|
164
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
165
|
-
class OrderRejected(BrokerResponseEvent):
|
|
166
|
-
order_id: uuid.UUID
|
|
167
|
-
reason: Models.RejectionReason
|
|
168
|
-
|
|
169
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
170
|
-
class OrderCancelled(BrokerResponseEvent):
|
|
171
|
-
order_id: uuid.UUID
|
|
172
|
-
|
|
173
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
174
|
-
class OrderExpired(BrokerResponseEvent):
|
|
175
|
-
order_id: uuid.UUID
|
|
176
|
-
|
|
177
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
178
|
-
class CancelRejected(BrokerResponseEvent):
|
|
179
|
-
order_id: uuid.UUID
|
|
180
|
-
reason: Models.RejectionReason
|
|
181
|
-
|
|
182
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
183
|
-
class ModifyRejected(BrokerResponseEvent):
|
|
184
|
-
order_id: uuid.UUID
|
|
185
|
-
reason: Models.RejectionReason
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
class BaseConsumer(abc.ABC):
|
|
189
|
-
"""
|
|
190
|
-
Base class for all consumers.
|
|
191
|
-
"""
|
|
192
|
-
|
|
193
|
-
def __init__(self) -> None:
|
|
194
|
-
self.queue: queue.Queue[Events.BaseEvent] = queue.Queue()
|
|
195
|
-
self._thread = threading.Thread(
|
|
196
|
-
target=self._consume, name=self.__class__.__name__, daemon=True
|
|
197
|
-
)
|
|
198
|
-
self._thread.start()
|
|
199
|
-
|
|
200
|
-
@abc.abstractmethod
|
|
201
|
-
def on_event(self, event: Events.BaseEvent) -> None:
|
|
202
|
-
pass
|
|
203
|
-
|
|
204
|
-
def receive(self, event: Events.BaseEvent) -> None:
|
|
205
|
-
self.queue.put(event)
|
|
206
|
-
|
|
207
|
-
def _consume(self) -> None:
|
|
208
|
-
while True:
|
|
209
|
-
event = self.queue.get()
|
|
210
|
-
if isinstance(event, Events.SystemShutdown):
|
|
211
|
-
self.queue.task_done()
|
|
212
|
-
break
|
|
213
|
-
self.on_event(event)
|
|
214
|
-
self.queue.task_done()
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
class EventBus:
|
|
218
|
-
"""
|
|
219
|
-
Event bus for publishing events to the consumers subscribed to them.
|
|
220
|
-
"""
|
|
221
|
-
|
|
222
|
-
def __init__(self) -> None:
|
|
223
|
-
self._subscriptions: defaultdict[type[Events.BaseEvent], list[BaseConsumer]] = (
|
|
224
|
-
defaultdict(list)
|
|
225
|
-
)
|
|
226
|
-
self._consumers: set[BaseConsumer] = set()
|
|
227
|
-
self._lock: threading.Lock = threading.Lock()
|
|
228
|
-
|
|
229
|
-
def subscribe(self, subscriber: BaseConsumer, event_type: type[Events.BaseEvent]):
|
|
230
|
-
with self._lock:
|
|
231
|
-
self._consumers.add(subscriber)
|
|
232
|
-
if subscriber not in self._subscriptions[event_type]:
|
|
233
|
-
self._subscriptions[event_type].append(subscriber)
|
|
234
|
-
|
|
235
|
-
def unsubscribe(self, subscriber: BaseConsumer):
|
|
236
|
-
with self._lock:
|
|
237
|
-
for consumer_list in self._subscriptions.values():
|
|
238
|
-
if subscriber in consumer_list:
|
|
239
|
-
consumer_list.remove(subscriber)
|
|
240
|
-
if not any(subscriber in cl for cl in self._subscriptions.values()):
|
|
241
|
-
self._consumers.discard(subscriber)
|
|
242
|
-
|
|
243
|
-
def publish(self, event: Events.BaseEvent) -> None:
|
|
244
|
-
with self._lock:
|
|
245
|
-
consumers = list(self._subscriptions[type(event)])
|
|
246
|
-
for consumer in consumers:
|
|
247
|
-
consumer.receive(event)
|
|
248
|
-
|
|
249
|
-
# Enable synchronous execution via wait_until_idle()
|
|
250
|
-
def wait_until_idle(self) -> None:
|
|
251
|
-
with self._lock:
|
|
252
|
-
consumers = list(self._consumers)
|
|
253
|
-
for consumer in consumers:
|
|
254
|
-
consumer.queue.join()
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
event_bus = EventBus()
|
|
258
|
-
"""
|
|
259
|
-
Global instance of `EventBus`.
|
|
260
|
-
"""
|
onesecondtrader/datafeeds.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import pandas as pd
|
|
3
|
-
import threading
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from onesecondtrader.core import Events, Models, event_bus, logger
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class DatafeedBase(abc.ABC):
|
|
10
|
-
"""
|
|
11
|
-
Base class for all datafeeds.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
def __init__(self) -> None:
|
|
15
|
-
self._is_connected: bool = False
|
|
16
|
-
self._watched_symbols: set[tuple[str, Models.RecordType]] = set()
|
|
17
|
-
self._lock: threading.Lock = threading.Lock()
|
|
18
|
-
|
|
19
|
-
@abc.abstractmethod
|
|
20
|
-
def watch(self, symbols: list[tuple[str, Models.RecordType]]) -> bool:
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
@abc.abstractmethod
|
|
24
|
-
def unwatch(self, symbols: list[str]) -> None:
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class SimulatedDatafeedCSV(DatafeedBase):
|
|
29
|
-
"""
|
|
30
|
-
CSV-based simulated datafeed for backtesting.
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
csv_path: str | Path = ""
|
|
34
|
-
artificial_delay: float = 0.0
|
|
35
|
-
|
|
36
|
-
def __init__(self) -> None:
|
|
37
|
-
super().__init__()
|
|
38
|
-
self._stop_event = threading.Event()
|
|
39
|
-
self._streaming_thread: threading.Thread | None = None
|
|
40
|
-
self._data_iterator: pd.io.parsers.readers.TextFileReader | None = None
|
|
41
|
-
self._connected_path: str | Path = ""
|
|
42
|
-
|
|
43
|
-
def watch(self, symbols: list[tuple[str, Models.RecordType]]) -> bool:
|
|
44
|
-
with self._lock:
|
|
45
|
-
if not self._is_connected:
|
|
46
|
-
try:
|
|
47
|
-
self._data_iterator = pd.read_csv(
|
|
48
|
-
Path(self.csv_path),
|
|
49
|
-
usecols=[
|
|
50
|
-
"ts_event",
|
|
51
|
-
"rtype",
|
|
52
|
-
"open",
|
|
53
|
-
"high",
|
|
54
|
-
"low",
|
|
55
|
-
"close",
|
|
56
|
-
"volume",
|
|
57
|
-
"symbol",
|
|
58
|
-
],
|
|
59
|
-
dtype={
|
|
60
|
-
"ts_event": int,
|
|
61
|
-
"rtype": int,
|
|
62
|
-
"open": int,
|
|
63
|
-
"high": int,
|
|
64
|
-
"low": int,
|
|
65
|
-
"close": int,
|
|
66
|
-
"volume": int,
|
|
67
|
-
"symbol": str,
|
|
68
|
-
},
|
|
69
|
-
chunksize=1,
|
|
70
|
-
)
|
|
71
|
-
self._is_connected = True
|
|
72
|
-
self._connected_path = self.csv_path
|
|
73
|
-
logger.info(
|
|
74
|
-
f"{self.__class__.__name__} connected to {self.csv_path}"
|
|
75
|
-
)
|
|
76
|
-
except Exception as e:
|
|
77
|
-
logger.error(f"{self.__class__.__name__} failed to connect: {e}")
|
|
78
|
-
self._data_iterator = None
|
|
79
|
-
self._is_connected = False
|
|
80
|
-
return False
|
|
81
|
-
elif self._connected_path != self.csv_path:
|
|
82
|
-
logger.warning(
|
|
83
|
-
"csv_path changed while connected; unwatch all symbols first"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
self._watched_symbols.update(symbols)
|
|
87
|
-
formatted = ", ".join(f"{s} ({r.name})" for s, r in symbols)
|
|
88
|
-
logger.info(f"{self.__class__.__name__} watching {formatted}")
|
|
89
|
-
|
|
90
|
-
if not self._streaming_thread or not self._streaming_thread.is_alive():
|
|
91
|
-
self._stop_event.clear()
|
|
92
|
-
self._streaming_thread = threading.Thread(
|
|
93
|
-
target=self._stream, name="CSVDatafeedStreaming", daemon=False
|
|
94
|
-
)
|
|
95
|
-
self._streaming_thread.start()
|
|
96
|
-
|
|
97
|
-
return True
|
|
98
|
-
|
|
99
|
-
def unwatch(self, symbols: list[str]) -> None:
|
|
100
|
-
thread_to_join = None
|
|
101
|
-
with self._lock:
|
|
102
|
-
symbols_set = set(symbols)
|
|
103
|
-
self._watched_symbols.difference_update(
|
|
104
|
-
{
|
|
105
|
-
(symbol, rtype)
|
|
106
|
-
for (symbol, rtype) in self._watched_symbols
|
|
107
|
-
if symbol in symbols_set
|
|
108
|
-
}
|
|
109
|
-
)
|
|
110
|
-
logger.info(f"{self.__class__.__name__} unwatched {', '.join(symbols)}")
|
|
111
|
-
if not self._watched_symbols:
|
|
112
|
-
self._stop_event.set()
|
|
113
|
-
thread_to_join = self._streaming_thread
|
|
114
|
-
self._streaming_thread = None
|
|
115
|
-
|
|
116
|
-
if thread_to_join and thread_to_join.is_alive():
|
|
117
|
-
thread_to_join.join(timeout=5.0)
|
|
118
|
-
if thread_to_join.is_alive():
|
|
119
|
-
logger.warning("Streaming thread did not terminate within timeout")
|
|
120
|
-
else:
|
|
121
|
-
logger.info(f"{self.__class__.__name__} disconnected")
|
|
122
|
-
|
|
123
|
-
def _stream(self) -> None:
|
|
124
|
-
if self._data_iterator is None:
|
|
125
|
-
logger.error("_stream called with no data iterator")
|
|
126
|
-
return
|
|
127
|
-
should_delay = self.artificial_delay > 0
|
|
128
|
-
delay_time = self.artificial_delay
|
|
129
|
-
while not self._stop_event.is_set():
|
|
130
|
-
try:
|
|
131
|
-
chunk = next(self._data_iterator)
|
|
132
|
-
row = chunk.iloc[0]
|
|
133
|
-
|
|
134
|
-
symbol = row["symbol"]
|
|
135
|
-
record_type = Models.RecordType(row["rtype"])
|
|
136
|
-
symbol_key = (symbol, record_type)
|
|
137
|
-
|
|
138
|
-
with self._lock:
|
|
139
|
-
if symbol_key not in self._watched_symbols:
|
|
140
|
-
continue
|
|
141
|
-
|
|
142
|
-
bar_event = Events.IncomingBar(
|
|
143
|
-
ts_event=pd.Timestamp(row["ts_event"], unit="ns", tz="UTC"),
|
|
144
|
-
symbol=symbol,
|
|
145
|
-
record_type=record_type,
|
|
146
|
-
open=row["open"] / 1e9,
|
|
147
|
-
high=row["high"] / 1e9,
|
|
148
|
-
low=row["low"] / 1e9,
|
|
149
|
-
close=row["close"] / 1e9,
|
|
150
|
-
volume=row["volume"],
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
event_bus.publish(bar_event)
|
|
154
|
-
event_bus.wait_until_idle()
|
|
155
|
-
|
|
156
|
-
if should_delay and self._stop_event.wait(delay_time):
|
|
157
|
-
break
|
|
158
|
-
except StopIteration:
|
|
159
|
-
logger.info("CSV datafeed reached end of file")
|
|
160
|
-
break
|
|
161
|
-
except Exception as e:
|
|
162
|
-
logger.error(f"CSV datafeed error reading data: {e}")
|
|
163
|
-
break
|
|
164
|
-
|
|
165
|
-
with self._lock:
|
|
166
|
-
self._data_iterator = None
|
|
167
|
-
self._is_connected = False
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
simulated_datafeed_csv = SimulatedDatafeedCSV()
|
|
171
|
-
"""
|
|
172
|
-
Global instance of `SimulatedDatafeedCSV`.
|
|
173
|
-
"""
|
onesecondtrader/indicators.py
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
OneSecondTrader's library of pre-built indicators.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import abc
|
|
6
|
-
import enum
|
|
7
|
-
import numpy as np
|
|
8
|
-
import threading
|
|
9
|
-
|
|
10
|
-
from collections import deque
|
|
11
|
-
from onesecondtrader.core import Events
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class BaseIndicator(abc.ABC):
|
|
15
|
-
"""
|
|
16
|
-
Base class for indicators. Subclasses must set the `name` property and implement
|
|
17
|
-
the `_compute_indicator()` method. See `SimpleMovingAverage` for an example.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
def __init__(self, max_history: int = 100) -> None:
|
|
21
|
-
self._lock = threading.Lock()
|
|
22
|
-
self._history: deque[float] = deque(maxlen=max(1, int(max_history)))
|
|
23
|
-
|
|
24
|
-
@property
|
|
25
|
-
@abc.abstractmethod
|
|
26
|
-
def name(self) -> str:
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
|
-
def update(self, incoming_bar: Events.IncomingBar) -> None:
|
|
30
|
-
_latest_value: float = self._compute_indicator(incoming_bar)
|
|
31
|
-
with self._lock:
|
|
32
|
-
self._history.append(_latest_value)
|
|
33
|
-
|
|
34
|
-
@abc.abstractmethod
|
|
35
|
-
def _compute_indicator(self, incoming_bar: Events.IncomingBar) -> float:
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def latest(self) -> float:
|
|
40
|
-
with self._lock:
|
|
41
|
-
return self._history[-1] if self._history else np.nan
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def history(self) -> deque[float]:
|
|
45
|
-
return self._history
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class InputSource(enum.Enum):
|
|
49
|
-
"""
|
|
50
|
-
Enum of supported input sources for indicators. Indicators with a `input_source`
|
|
51
|
-
parameter can be configured to use one of these sources for their calculations.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
OPEN = enum.auto()
|
|
55
|
-
HIGH = enum.auto()
|
|
56
|
-
LOW = enum.auto()
|
|
57
|
-
CLOSE = enum.auto()
|
|
58
|
-
VOLUME = enum.auto()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class SimpleMovingAverage(BaseIndicator):
|
|
62
|
-
"""
|
|
63
|
-
Simple Moving Average (SMA) indicator. Can be configured to use different input
|
|
64
|
-
sources (see `InputSource` enum, default is `InputSource.CLOSE`).
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
def __init__(
|
|
68
|
-
self,
|
|
69
|
-
period: int = 200,
|
|
70
|
-
max_history: int = 100,
|
|
71
|
-
input_source: InputSource = InputSource.CLOSE,
|
|
72
|
-
) -> None:
|
|
73
|
-
super().__init__(max_history=max_history)
|
|
74
|
-
self.period: int = max(1, int(period))
|
|
75
|
-
self.input_source: InputSource = input_source
|
|
76
|
-
self._window: deque[float] = deque(maxlen=self.period)
|
|
77
|
-
|
|
78
|
-
@property
|
|
79
|
-
def name(self) -> str:
|
|
80
|
-
return f"SMA_{self.period}_{self.input_source.name}"
|
|
81
|
-
|
|
82
|
-
def _compute_indicator(self, incoming_bar: Events.IncomingBar) -> float:
|
|
83
|
-
value: float = self._extract_input(incoming_bar)
|
|
84
|
-
self._window.append(value)
|
|
85
|
-
if len(self._window) < self.period:
|
|
86
|
-
return np.nan
|
|
87
|
-
return sum(self._window) / self.period
|
|
88
|
-
|
|
89
|
-
def _extract_input(self, incoming_bar: Events.IncomingBar) -> float:
|
|
90
|
-
match self.input_source:
|
|
91
|
-
case InputSource.OPEN:
|
|
92
|
-
return incoming_bar.open
|
|
93
|
-
case InputSource.HIGH:
|
|
94
|
-
return incoming_bar.high
|
|
95
|
-
case InputSource.LOW:
|
|
96
|
-
return incoming_bar.low
|
|
97
|
-
case InputSource.CLOSE:
|
|
98
|
-
return incoming_bar.close
|
|
99
|
-
case InputSource.VOLUME:
|
|
100
|
-
return (
|
|
101
|
-
float(incoming_bar.volume)
|
|
102
|
-
if incoming_bar.volume is not None
|
|
103
|
-
else np.nan
|
|
104
|
-
)
|
|
105
|
-
case _:
|
|
106
|
-
return incoming_bar.close
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
onesecondtrader/brokers.py,sha256=fDElmF0y6KD6NIVBX0lo8Z-nQzpe-Rp2J2C75TrbSNg,2779
|
|
3
|
-
onesecondtrader/core.py,sha256=5H-JydCnMOedLw-GrSB1aooDj6s0R1gpVwI_JgxZpzA,7495
|
|
4
|
-
onesecondtrader/datafeeds.py,sha256=IlMipy1OFLx8aMBa46d74Mm3rhhb-uR905odhVP_uX0,6149
|
|
5
|
-
onesecondtrader/indicators.py,sha256=wGn-5v8L1gepMP45KcVrEo-f2ReOCD3r8lva9aEIUnY,3199
|
|
6
|
-
onesecondtrader-0.22.0.dist-info/METADATA,sha256=lp8FV-Gaqm8IH36ubGu0EPzCXyPog5PfOVYRv3LBJZc,9682
|
|
7
|
-
onesecondtrader-0.22.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
8
|
-
onesecondtrader-0.22.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
9
|
-
onesecondtrader-0.22.0.dist-info/RECORD,,
|
|
File without changes
|