onesecondtrader 0.18.0__py3-none-any.whl → 0.20.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/core.py +108 -7
- onesecondtrader/datafeeds.py +172 -0
- {onesecondtrader-0.18.0.dist-info → onesecondtrader-0.20.0.dist-info}/METADATA +1 -1
- onesecondtrader-0.20.0.dist-info/RECORD +8 -0
- onesecondtrader-0.18.0.dist-info/RECORD +0 -7
- {onesecondtrader-0.18.0.dist-info → onesecondtrader-0.20.0.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.18.0.dist-info → onesecondtrader-0.20.0.dist-info}/licenses/LICENSE +0 -0
onesecondtrader/core.py
CHANGED
|
@@ -5,6 +5,7 @@ Core module containing the backbone of OneSecondTrader's event-driven architectu
|
|
|
5
5
|
import abc
|
|
6
6
|
import dataclasses
|
|
7
7
|
import enum
|
|
8
|
+
import logging
|
|
8
9
|
import pandas as pd
|
|
9
10
|
import queue
|
|
10
11
|
import threading
|
|
@@ -13,6 +14,13 @@ import uuid
|
|
|
13
14
|
from collections import defaultdict
|
|
14
15
|
|
|
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
|
+
|
|
16
24
|
class Models:
|
|
17
25
|
"""
|
|
18
26
|
Namespace for all models.
|
|
@@ -34,12 +42,41 @@ class Models:
|
|
|
34
42
|
STOP = enum.auto()
|
|
35
43
|
STOP_LIMIT = enum.auto()
|
|
36
44
|
|
|
45
|
+
class OrderRejectionReason(enum.Enum):
|
|
46
|
+
INSUFFICIENT_FUNDS = enum.auto()
|
|
47
|
+
MARKET_CLOSED = enum.auto()
|
|
48
|
+
UNKNOWN = enum.auto()
|
|
49
|
+
|
|
50
|
+
class CancelRejectionReason(enum.Enum):
|
|
51
|
+
ORDER_ALREADY_FILLED = enum.auto()
|
|
52
|
+
ORDER_ALREADY_CANCELLED = enum.auto()
|
|
53
|
+
ORDER_PENDING_EXECUTION = enum.auto()
|
|
54
|
+
MARKET_CLOSED = enum.auto()
|
|
55
|
+
UNKNOWN = enum.auto()
|
|
56
|
+
|
|
57
|
+
class ModifyRejectionReason(enum.Enum):
|
|
58
|
+
ORDER_ALREADY_FILLED = enum.auto()
|
|
59
|
+
ORDER_ALREADY_CANCELLED = enum.auto()
|
|
60
|
+
ORDER_PENDING_EXECUTION = enum.auto()
|
|
61
|
+
ORDER_NOT_FOUND = enum.auto()
|
|
62
|
+
INVALID_PRICE = enum.auto()
|
|
63
|
+
INVALID_QUANTITY = enum.auto()
|
|
64
|
+
MARKET_CLOSED = enum.auto()
|
|
65
|
+
UNKNOWN = enum.auto()
|
|
66
|
+
|
|
67
|
+
class TimeInForce(enum.Enum):
|
|
68
|
+
GTC = enum.auto()
|
|
69
|
+
DAY = enum.auto()
|
|
70
|
+
IOC = enum.auto()
|
|
71
|
+
FOK = enum.auto()
|
|
72
|
+
|
|
37
73
|
|
|
38
74
|
class Events:
|
|
39
75
|
"""
|
|
40
76
|
Namespace for all events.
|
|
41
77
|
"""
|
|
42
78
|
|
|
79
|
+
# BASE EVENT
|
|
43
80
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
44
81
|
class BaseEvent:
|
|
45
82
|
ts_event: pd.Timestamp = dataclasses.field(
|
|
@@ -48,12 +85,20 @@ class Events:
|
|
|
48
85
|
|
|
49
86
|
# SYSTEM EVENTS
|
|
50
87
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
51
|
-
class
|
|
88
|
+
class SystemEvent(BaseEvent):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
92
|
+
class SystemShutdown(SystemEvent):
|
|
52
93
|
pass
|
|
53
94
|
|
|
54
95
|
# MARKET EVENTS
|
|
55
96
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
56
|
-
class
|
|
97
|
+
class MarketEvent(BaseEvent):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
101
|
+
class IncomingBar(MarketEvent):
|
|
57
102
|
ts_event: pd.Timestamp
|
|
58
103
|
symbol: str
|
|
59
104
|
record_type: Models.RecordType
|
|
@@ -63,29 +108,83 @@ class Events:
|
|
|
63
108
|
close: float
|
|
64
109
|
volume: int | None = None
|
|
65
110
|
|
|
66
|
-
# BROKER
|
|
111
|
+
# BROKER REQUESTS EVENTS
|
|
67
112
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
68
|
-
class
|
|
69
|
-
|
|
113
|
+
class BrokerRequestEvent(BaseEvent):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
117
|
+
class SubmitOrder(BrokerRequestEvent):
|
|
118
|
+
order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
70
119
|
symbol: str
|
|
71
120
|
order_type: Models.OrderType
|
|
72
121
|
side: Models.OrderSide
|
|
73
122
|
quantity: float
|
|
74
123
|
limit_price: float | None = None
|
|
75
124
|
stop_price: float | None = None
|
|
125
|
+
time_in_force: Models.TimeInForce = Models.TimeInForce.GTC
|
|
126
|
+
|
|
127
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
128
|
+
class ModifyOrder(BrokerRequestEvent):
|
|
129
|
+
order_id: uuid.UUID
|
|
130
|
+
quantity: float | None = None
|
|
131
|
+
limit_price: float | None = None
|
|
132
|
+
stop_price: float | None = None
|
|
133
|
+
|
|
134
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
135
|
+
class CancelOrder(BrokerRequestEvent):
|
|
136
|
+
order_id: uuid.UUID
|
|
76
137
|
|
|
77
138
|
# BROKER RESPONSE EVENTS
|
|
78
139
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
79
|
-
class
|
|
140
|
+
class BrokerResponseEvent(BaseEvent):
|
|
141
|
+
ts_broker: pd.Timestamp
|
|
142
|
+
|
|
143
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
144
|
+
class OrderSubmitted(BrokerResponseEvent):
|
|
145
|
+
order_id: uuid.UUID
|
|
146
|
+
broker_order_id: str | None = None
|
|
147
|
+
|
|
148
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
149
|
+
class OrderModified(BrokerResponseEvent):
|
|
150
|
+
order_id: uuid.UUID
|
|
151
|
+
broker_order_id: str | None = None
|
|
152
|
+
|
|
153
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
154
|
+
class Fill(BrokerResponseEvent):
|
|
80
155
|
fill_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
81
156
|
broker_fill_id: str | None = None
|
|
82
157
|
associated_order_id: uuid.UUID
|
|
158
|
+
symbol: str
|
|
83
159
|
side: Models.OrderSide
|
|
84
160
|
quantity_filled: float
|
|
85
161
|
fill_price: float
|
|
86
162
|
commission: float
|
|
87
163
|
exchange: str = "SIMULATED"
|
|
88
164
|
|
|
165
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
166
|
+
class OrderRejected(BrokerResponseEvent):
|
|
167
|
+
order_id: uuid.UUID
|
|
168
|
+
reason: Models.OrderRejectionReason
|
|
169
|
+
|
|
170
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
171
|
+
class OrderCancelled(BrokerResponseEvent):
|
|
172
|
+
order_id: uuid.UUID
|
|
173
|
+
|
|
174
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
175
|
+
class OrderExpired(BrokerResponseEvent):
|
|
176
|
+
order_id: uuid.UUID
|
|
177
|
+
|
|
178
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
179
|
+
class CancelRejected(BrokerResponseEvent):
|
|
180
|
+
order_id: uuid.UUID
|
|
181
|
+
reason: Models.CancelRejectionReason
|
|
182
|
+
|
|
183
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
184
|
+
class ModifyRejected(BrokerResponseEvent):
|
|
185
|
+
order_id: uuid.UUID
|
|
186
|
+
reason: Models.ModifyRejectionReason
|
|
187
|
+
|
|
89
188
|
|
|
90
189
|
class BaseConsumer(abc.ABC):
|
|
91
190
|
"""
|
|
@@ -94,7 +193,9 @@ class BaseConsumer(abc.ABC):
|
|
|
94
193
|
|
|
95
194
|
def __init__(self) -> None:
|
|
96
195
|
self._queue: queue.Queue[Events.BaseEvent] = queue.Queue()
|
|
97
|
-
self._thread = threading.Thread(
|
|
196
|
+
self._thread = threading.Thread(
|
|
197
|
+
target=self._consume, name=self.__class__.__name__, daemon=True
|
|
198
|
+
)
|
|
98
199
|
self._thread.start()
|
|
99
200
|
|
|
100
201
|
@abc.abstractmethod
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
|
|
155
|
+
if should_delay and self._stop_event.wait(delay_time):
|
|
156
|
+
break
|
|
157
|
+
except StopIteration:
|
|
158
|
+
logger.info("CSV datafeed reached end of file")
|
|
159
|
+
break
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"CSV datafeed error reading data: {e}")
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
with self._lock:
|
|
165
|
+
self._data_iterator = None
|
|
166
|
+
self._is_connected = False
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
simulated_datafeed_csv = SimulatedDatafeedCSV()
|
|
170
|
+
"""
|
|
171
|
+
Global instance of SimulatedDatafeedCSV.
|
|
172
|
+
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onesecondtrader
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: The Trading Infrastructure Toolkit for Python. Research, simulate, and deploy algorithmic trading strategies — all in one place.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Nils P. Kujath
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
onesecondtrader/core.py,sha256=UH8nVc_tZX-xHEaCp1YCyMeHDOtQTbP6sr8a9bqwSyw,7115
|
|
3
|
+
onesecondtrader/datafeeds.py,sha256=KXK2duFRFPo7KuNbG7AqIu-LGXtuuDj20aTOGBE4Cm4,6103
|
|
4
|
+
onesecondtrader/indicators.py,sha256=wGn-5v8L1gepMP45KcVrEo-f2ReOCD3r8lva9aEIUnY,3199
|
|
5
|
+
onesecondtrader-0.20.0.dist-info/METADATA,sha256=6FN1iee59eZiuQz1Y8NsY4EeOhayXxx6xGsmKt3UdOg,9682
|
|
6
|
+
onesecondtrader-0.20.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
7
|
+
onesecondtrader-0.20.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
8
|
+
onesecondtrader-0.20.0.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
onesecondtrader/core.py,sha256=uMy2eYLtzfSPC_99kjft7L3DL4SRi1ZBnnSYyd_XrIo,3921
|
|
3
|
-
onesecondtrader/indicators.py,sha256=wGn-5v8L1gepMP45KcVrEo-f2ReOCD3r8lva9aEIUnY,3199
|
|
4
|
-
onesecondtrader-0.18.0.dist-info/METADATA,sha256=48Xk2OPiF57MkcpqFnyAG328SWljqyTEO5XmFS0VDrI,9682
|
|
5
|
-
onesecondtrader-0.18.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
6
|
-
onesecondtrader-0.18.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
-
onesecondtrader-0.18.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|