onesecondtrader 0.21.0__tar.gz → 0.23.0__tar.gz
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-0.21.0 → onesecondtrader-0.23.0}/PKG-INFO +1 -1
- {onesecondtrader-0.21.0 → onesecondtrader-0.23.0}/pyproject.toml +1 -1
- onesecondtrader-0.23.0/src/onesecondtrader/core/__init__.py +0 -0
- onesecondtrader-0.23.0/src/onesecondtrader/core/component.py +119 -0
- onesecondtrader-0.23.0/src/onesecondtrader/core/eventbus.py +85 -0
- onesecondtrader-0.23.0/src/onesecondtrader/core/events.py +224 -0
- onesecondtrader-0.23.0/src/onesecondtrader/core/models.py +70 -0
- onesecondtrader-0.21.0/src/onesecondtrader/core.py +0 -234
- onesecondtrader-0.21.0/src/onesecondtrader/datafeeds.py +0 -172
- onesecondtrader-0.21.0/src/onesecondtrader/indicators.py +0 -106
- {onesecondtrader-0.21.0 → onesecondtrader-0.23.0}/LICENSE +0 -0
- {onesecondtrader-0.21.0 → onesecondtrader-0.23.0}/README.md +0 -0
- {onesecondtrader-0.21.0 → onesecondtrader-0.23.0}/src/onesecondtrader/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onesecondtrader
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "onesecondtrader"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.23.0"
|
|
4
4
|
description = "The Trading Infrastructure Toolkit for Python. Research, simulate, and deploy algorithmic trading strategies — all in one place."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Nils P. Kujath",email = "63961429+NilsKujath@users.noreply.github.com"}
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read first: [`events.py`](./events.md), [`eventbus.py`](./eventbus.md).
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
This module defines the `Component` class.
|
|
6
|
+
|
|
7
|
+
In an event-driven system, components interact exclusively via event messages.
|
|
8
|
+
The `Component` class is an abstract base class that defines a common interface for
|
|
9
|
+
publishing and receiving such messages and is intended to be subclassed by all system
|
|
10
|
+
components.
|
|
11
|
+
---
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import abc
|
|
15
|
+
import queue
|
|
16
|
+
import threading
|
|
17
|
+
|
|
18
|
+
from .events import Event
|
|
19
|
+
from .eventbus import EventBus
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Component(abc.ABC):
|
|
23
|
+
"""
|
|
24
|
+
Abstract base class for all system components.
|
|
25
|
+
|
|
26
|
+
Incoming events are delivered via `receive()` and stored in an internal queue.
|
|
27
|
+
A dedicated thread runs an event loop that consumes queued events and dispatches
|
|
28
|
+
them to `_process()`, which subclasses must implement.
|
|
29
|
+
For publication, `publish()` forwards events to the event bus.
|
|
30
|
+
The `join_queue()` method is used by the event bus to block until all queued events
|
|
31
|
+
have been processed, which is useful for deterministic, stepwise execution in
|
|
32
|
+
backtesting.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, eventbus: EventBus) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Initialize the component and start its event-loop thread.
|
|
38
|
+
|
|
39
|
+
Stores a reference to the event bus, creates an internal queue for incoming
|
|
40
|
+
events, and starts a non-daemon thread for the event loop that runs until
|
|
41
|
+
shutdown.
|
|
42
|
+
"""
|
|
43
|
+
self._event_bus = eventbus
|
|
44
|
+
self._incoming_event_queue: queue.Queue[Event | None] = queue.Queue()
|
|
45
|
+
self._thread: threading.Thread = threading.Thread(
|
|
46
|
+
target=self._event_loop,
|
|
47
|
+
name=self.__class__.__name__,
|
|
48
|
+
daemon=False,
|
|
49
|
+
)
|
|
50
|
+
self._thread.start()
|
|
51
|
+
|
|
52
|
+
def publish(self, event: Event) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Wrapper for the event bus's `publish()` method.
|
|
55
|
+
|
|
56
|
+
For convenience, this method can be used by subclasses to publish events via the
|
|
57
|
+
event bus's publication mechanism.
|
|
58
|
+
"""
|
|
59
|
+
self._event_bus.publish(event)
|
|
60
|
+
|
|
61
|
+
def receive(self, event: Event) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Store incoming events in the component's queue.
|
|
64
|
+
|
|
65
|
+
This method is typically called by the event bus when an event is published that
|
|
66
|
+
the component has subscribed to.
|
|
67
|
+
It simply puts the event in the queue for processing by the subscribed
|
|
68
|
+
component.
|
|
69
|
+
"""
|
|
70
|
+
self._incoming_event_queue.put(event)
|
|
71
|
+
|
|
72
|
+
def join_queue(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Block until all queued events have been processed.
|
|
75
|
+
|
|
76
|
+
This method is used by the event bus to block until all queued events have been
|
|
77
|
+
processed.
|
|
78
|
+
This is useful for deterministic, stepwise execution in backtesting.
|
|
79
|
+
"""
|
|
80
|
+
self._incoming_event_queue.join()
|
|
81
|
+
|
|
82
|
+
def _event_loop(self) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Event-processing loop executed in the component’s dedicated thread.
|
|
85
|
+
|
|
86
|
+
The loop continuously retrieves events from the `incoming_event_queue`,
|
|
87
|
+
processes each event via `_process()`, and signals completion with
|
|
88
|
+
`task_done()`.
|
|
89
|
+
Execution terminates upon receipt of a `None` sentinel, at which point
|
|
90
|
+
`_shutdown()` is invoked before exiting the thread to ensure proper cleanup.
|
|
91
|
+
"""
|
|
92
|
+
while True:
|
|
93
|
+
incoming_event = self._incoming_event_queue.get()
|
|
94
|
+
if incoming_event is None:
|
|
95
|
+
self._shutdown()
|
|
96
|
+
self._incoming_event_queue.task_done()
|
|
97
|
+
break
|
|
98
|
+
self._process(incoming_event)
|
|
99
|
+
self._incoming_event_queue.task_done()
|
|
100
|
+
|
|
101
|
+
@abc.abstractmethod
|
|
102
|
+
def _process(self, event: Event) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Process a single event.
|
|
105
|
+
|
|
106
|
+
This method is invoked by the event loop for each incoming event.
|
|
107
|
+
Subclasses must implement it to define event-handling behavior.
|
|
108
|
+
"""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def _shutdown(self) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Perform cleanup when the component is shutting down.
|
|
114
|
+
|
|
115
|
+
Called by `_event_loop()` when a `None` sentinel is received.
|
|
116
|
+
Subclasses may override this method to perform cleanup tasks such as closing
|
|
117
|
+
files or connections before the thread exits.
|
|
118
|
+
"""
|
|
119
|
+
pass
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read first: [`events.py`](./events.md), [`component.py`](./component.md).
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
This module defines the `EventBus` class.
|
|
6
|
+
|
|
7
|
+
The event bus provides a publish–subscribe mechanism for event-driven systems.
|
|
8
|
+
It maintains subscriptions between event types and components and delivers
|
|
9
|
+
published events to all subscribed components in a thread-safe manner.
|
|
10
|
+
---
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import collections
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
from .component import Component
|
|
17
|
+
from .events import Event
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EventBus:
|
|
21
|
+
"""
|
|
22
|
+
Central event-dispatch mechanism for the system.
|
|
23
|
+
|
|
24
|
+
The event bus manages subscriptions between event types and components and
|
|
25
|
+
forwards published events to all subscribed components.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initialize the event bus.
|
|
31
|
+
|
|
32
|
+
Sets up internal data structures for managing subscriptions and registered
|
|
33
|
+
components and initializes a lock to ensure thread-safe access.
|
|
34
|
+
"""
|
|
35
|
+
self._subscriptions: collections.defaultdict[type[Event], set[Component]] = (
|
|
36
|
+
collections.defaultdict(set)
|
|
37
|
+
)
|
|
38
|
+
self._components: set[Component] = set()
|
|
39
|
+
self._lock: threading.Lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
def subscribe(self, subscriber: Component, *event_types: type[Event]) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Subscribe a component to one or more event types.
|
|
44
|
+
|
|
45
|
+
The subscriber will receive all future events whose type matches one of the
|
|
46
|
+
specified event types.
|
|
47
|
+
"""
|
|
48
|
+
with self._lock:
|
|
49
|
+
self._components.add(subscriber)
|
|
50
|
+
for event_type in event_types:
|
|
51
|
+
self._subscriptions[event_type].add(subscriber)
|
|
52
|
+
|
|
53
|
+
def unsubscribe(self, subscriber: Component) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Remove a component from all event subscriptions.
|
|
56
|
+
|
|
57
|
+
After unsubscription, the component will no longer receive published events.
|
|
58
|
+
"""
|
|
59
|
+
with self._lock:
|
|
60
|
+
for component_set in self._subscriptions.values():
|
|
61
|
+
component_set.discard(subscriber)
|
|
62
|
+
self._components.discard(subscriber)
|
|
63
|
+
|
|
64
|
+
def publish(self, event: Event) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Publish an event to all subscribed components.
|
|
67
|
+
|
|
68
|
+
The event is delivered to each subscriber by invoking its `receive()` method.
|
|
69
|
+
"""
|
|
70
|
+
with self._lock:
|
|
71
|
+
components = self._subscriptions[type(event)].copy()
|
|
72
|
+
for component in components:
|
|
73
|
+
component.receive(event)
|
|
74
|
+
|
|
75
|
+
def wait_until_idle(self) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Block until all components have processed their queued events.
|
|
78
|
+
|
|
79
|
+
This method is primarily used in backtesting to enforce deterministic,
|
|
80
|
+
system-wide synchronization.
|
|
81
|
+
"""
|
|
82
|
+
with self._lock:
|
|
83
|
+
components = self._components.copy()
|
|
84
|
+
for component in components:
|
|
85
|
+
component.join_queue()
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read first: [`models.py`](./models.md).
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
This module provides all event message classes used in the system.
|
|
6
|
+
|
|
7
|
+
In an event-driven system, components communicate by sending event messages to each
|
|
8
|
+
other. An event message is an immutable object that contains all information relevant
|
|
9
|
+
to a specific occurrence, such as incoming market data, order submissions, fills, or
|
|
10
|
+
other domain-specific events.
|
|
11
|
+
---
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from .models import BarPeriod, OrderType, OrderSide
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
23
|
+
class Event:
|
|
24
|
+
"""
|
|
25
|
+
Base class for all event messages in the system.
|
|
26
|
+
|
|
27
|
+
All events include a timestamp indicating when the event was created or received.
|
|
28
|
+
Subclasses define specific event types with additional fields relevant to that
|
|
29
|
+
event.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
ts_event: pd.Timestamp = dataclasses.field(
|
|
33
|
+
default_factory=lambda: pd.Timestamp.now(tz="UTC")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
38
|
+
class ReceivedNewBar(Event):
|
|
39
|
+
"""
|
|
40
|
+
Event indicating a new bar of market data has been received.
|
|
41
|
+
|
|
42
|
+
Contains OHLCV data for a specific symbol and bar period. This event is typically
|
|
43
|
+
emitted by data feed components when new market data arrives.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
ts_event: pd.Timestamp
|
|
47
|
+
symbol: str
|
|
48
|
+
bar_period: BarPeriod
|
|
49
|
+
open: float
|
|
50
|
+
high: float
|
|
51
|
+
low: float
|
|
52
|
+
close: float
|
|
53
|
+
volume: int | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
57
|
+
class ProcessedBar(Event):
|
|
58
|
+
"""
|
|
59
|
+
Event indicating a bar has been processed and enriched with indicator values.
|
|
60
|
+
|
|
61
|
+
Contains the same OHLCV data as ReceivedNewBar, plus a dictionary of computed
|
|
62
|
+
indicator values. This event is typically emitted after strategies or other
|
|
63
|
+
components have calculated technical indicators for the bar.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
ts_event: pd.Timestamp
|
|
67
|
+
symbol: str
|
|
68
|
+
bar_period: BarPeriod
|
|
69
|
+
open: float
|
|
70
|
+
high: float
|
|
71
|
+
low: float
|
|
72
|
+
close: float
|
|
73
|
+
volume: int | None = None
|
|
74
|
+
indicators: dict[str, float] = dataclasses.field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
78
|
+
class RequestOrderSubmission(Event):
|
|
79
|
+
"""
|
|
80
|
+
Event requesting submission of a new order with the broker.
|
|
81
|
+
|
|
82
|
+
Contains all parameters needed to submit an order, including symbol, order type,
|
|
83
|
+
side, quantity, and optional limit/stop prices. A unique order_id is automatically
|
|
84
|
+
generated if not provided.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
88
|
+
symbol: str
|
|
89
|
+
order_type: OrderType
|
|
90
|
+
side: OrderSide
|
|
91
|
+
quantity: float
|
|
92
|
+
limit_price: float | None = None
|
|
93
|
+
stop_price: float | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
97
|
+
class RequestOrderModification(Event):
|
|
98
|
+
"""
|
|
99
|
+
Event requesting modification of an existing order.
|
|
100
|
+
|
|
101
|
+
Identifies the order to modify by order_id and symbol. Any of quantity, limit_price,
|
|
102
|
+
or stop_price can be updated by providing new values.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
symbol: str
|
|
106
|
+
order_id: uuid.UUID
|
|
107
|
+
quantity: float | None = None
|
|
108
|
+
limit_price: float | None = None
|
|
109
|
+
stop_price: float | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
113
|
+
class RequestOrderCancellation(Event):
|
|
114
|
+
"""
|
|
115
|
+
Event requesting cancellation of an existing order.
|
|
116
|
+
|
|
117
|
+
Identifies the order to cancel by order_id and symbol.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
symbol: str
|
|
121
|
+
order_id: uuid.UUID
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
125
|
+
class AcceptedOrderSubmission(Event):
|
|
126
|
+
"""
|
|
127
|
+
Event indicating the broker has accepted an order submission.
|
|
128
|
+
|
|
129
|
+
Contains the system's order_id and optionally the broker's order_id if provided
|
|
130
|
+
by the broker.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
order_id: uuid.UUID
|
|
134
|
+
broker_order_id: str | None = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
138
|
+
class AcceptedOrderModification(Event):
|
|
139
|
+
"""
|
|
140
|
+
Event indicating the broker has accepted an order modification.
|
|
141
|
+
|
|
142
|
+
Contains the system's order_id and optionally the broker's order_id if provided
|
|
143
|
+
by the broker.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
order_id: uuid.UUID
|
|
147
|
+
broker_order_id: str | None = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
151
|
+
class AcceptedOrderCancellation(Event):
|
|
152
|
+
"""
|
|
153
|
+
Event indicating the broker has accepted an order cancellation.
|
|
154
|
+
|
|
155
|
+
Contains the system's order_id for the cancelled order.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
order_id: uuid.UUID
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
162
|
+
class RejectedOrderSubmission(Event):
|
|
163
|
+
"""
|
|
164
|
+
Event indicating the broker has rejected an order submission.
|
|
165
|
+
|
|
166
|
+
Contains the system's order_id for the rejected order.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
order_id: uuid.UUID
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
173
|
+
class RejectedOrderModification(Event):
|
|
174
|
+
"""
|
|
175
|
+
Event indicating the broker has rejected an order modification.
|
|
176
|
+
|
|
177
|
+
Contains the system's order_id for the order that could not be modified.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
order_id: uuid.UUID
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
184
|
+
class RejectedOrderCancellation(Event):
|
|
185
|
+
"""
|
|
186
|
+
Event indicating the broker has rejected an order cancellation.
|
|
187
|
+
|
|
188
|
+
Contains the system's order_id for the order that could not be cancelled.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
order_id: uuid.UUID
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
195
|
+
class OrderFilled(Event):
|
|
196
|
+
"""
|
|
197
|
+
Event indicating an order has been filled (fully or partially).
|
|
198
|
+
|
|
199
|
+
Contains details about the fill including quantity, price, commission, and the
|
|
200
|
+
associated order. A unique fill_id is automatically generated if not provided.
|
|
201
|
+
The broker may optionally provide a broker_fill_id.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
fill_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
205
|
+
broker_fill_id: str | None = None
|
|
206
|
+
associated_order_id: uuid.UUID
|
|
207
|
+
symbol: str
|
|
208
|
+
side: OrderSide
|
|
209
|
+
quantity_filled: float
|
|
210
|
+
fill_price: float
|
|
211
|
+
commission: float
|
|
212
|
+
exchange: str = "SIMULATED"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
216
|
+
class OrderExpired(Event):
|
|
217
|
+
"""
|
|
218
|
+
Event indicating an order has expired without being filled.
|
|
219
|
+
|
|
220
|
+
Contains the system's order_id for the expired order. This typically occurs for
|
|
221
|
+
time-limited orders that were not filled before their expiration time.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
order_id: uuid.UUID
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
---
|
|
3
|
+
This module provides core domain models used throughout the system.
|
|
4
|
+
|
|
5
|
+
Domain models are enumerations that define the vocabulary and valid values for key
|
|
6
|
+
concepts in the trading system. These models are used across all components to ensure
|
|
7
|
+
type safety and consistency when representing domain-specific concepts.
|
|
8
|
+
---
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BarPeriod(enum.Enum):
|
|
15
|
+
"""
|
|
16
|
+
Time period for aggregating market data into bars.
|
|
17
|
+
|
|
18
|
+
Defines the standard time intervals used for OHLCV bar data.
|
|
19
|
+
Each bar represents market activity aggregated over the specified period.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
SECOND = 32
|
|
23
|
+
MINUTE = 33
|
|
24
|
+
HOUR = 34
|
|
25
|
+
DAY = 35
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OrderType(enum.Enum):
|
|
29
|
+
"""
|
|
30
|
+
Type of order to submit to the broker.
|
|
31
|
+
|
|
32
|
+
Defines the execution behavior for an order.
|
|
33
|
+
Market orders execute immediately at current market price.
|
|
34
|
+
Limit orders execute only at a specified price or better.
|
|
35
|
+
Stop orders become market orders when a trigger price is reached.
|
|
36
|
+
Stop-limit orders become limit orders when a trigger price is reached.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
MARKET = enum.auto()
|
|
40
|
+
LIMIT = enum.auto()
|
|
41
|
+
STOP = enum.auto()
|
|
42
|
+
STOP_LIMIT = enum.auto()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OrderSide(enum.Enum):
|
|
46
|
+
"""
|
|
47
|
+
Direction of an order or position.
|
|
48
|
+
|
|
49
|
+
Indicates whether an order is buying or selling a security.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
BUY = enum.auto()
|
|
53
|
+
SELL = enum.auto()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class InputSource(enum.Enum):
|
|
57
|
+
"""
|
|
58
|
+
Source field from bar data to use as input for indicators.
|
|
59
|
+
|
|
60
|
+
Specifies which component of OHLCV bar data should be used when calculating
|
|
61
|
+
technical indicators.
|
|
62
|
+
For example, many indicators use CLOSE prices per default, but could also be
|
|
63
|
+
calculated based on other fields.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
OPEN = enum.auto()
|
|
67
|
+
HIGH = enum.auto()
|
|
68
|
+
LOW = enum.auto()
|
|
69
|
+
CLOSE = enum.auto()
|
|
70
|
+
VOLUME = enum.auto()
|
|
@@ -1,234 +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
|
-
# BROKER REQUESTS EVENTS
|
|
98
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
99
|
-
class BrokerRequestEvent(BaseEvent):
|
|
100
|
-
pass
|
|
101
|
-
|
|
102
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
103
|
-
class SubmitOrder(BrokerRequestEvent):
|
|
104
|
-
order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
105
|
-
symbol: str
|
|
106
|
-
order_type: Models.OrderType
|
|
107
|
-
side: Models.OrderSide
|
|
108
|
-
quantity: float
|
|
109
|
-
limit_price: float | None = None
|
|
110
|
-
stop_price: float | None = None
|
|
111
|
-
time_in_force: Models.TimeInForce = Models.TimeInForce.GTC
|
|
112
|
-
|
|
113
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
114
|
-
class ModifyOrder(BrokerRequestEvent):
|
|
115
|
-
order_id: uuid.UUID
|
|
116
|
-
quantity: float | None = None
|
|
117
|
-
limit_price: float | None = None
|
|
118
|
-
stop_price: float | None = None
|
|
119
|
-
|
|
120
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
121
|
-
class CancelOrder(BrokerRequestEvent):
|
|
122
|
-
order_id: uuid.UUID
|
|
123
|
-
|
|
124
|
-
# BROKER RESPONSE EVENTS
|
|
125
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
126
|
-
class BrokerResponseEvent(BaseEvent):
|
|
127
|
-
ts_broker: pd.Timestamp
|
|
128
|
-
|
|
129
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
130
|
-
class OrderSubmitted(BrokerResponseEvent):
|
|
131
|
-
order_id: uuid.UUID
|
|
132
|
-
broker_order_id: str | None = None
|
|
133
|
-
|
|
134
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
135
|
-
class OrderModified(BrokerResponseEvent):
|
|
136
|
-
order_id: uuid.UUID
|
|
137
|
-
broker_order_id: str | None = None
|
|
138
|
-
|
|
139
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
140
|
-
class Fill(BrokerResponseEvent):
|
|
141
|
-
fill_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
142
|
-
broker_fill_id: str | None = None
|
|
143
|
-
associated_order_id: uuid.UUID
|
|
144
|
-
symbol: str
|
|
145
|
-
side: Models.OrderSide
|
|
146
|
-
quantity_filled: float
|
|
147
|
-
fill_price: float
|
|
148
|
-
commission: float
|
|
149
|
-
exchange: str = "SIMULATED"
|
|
150
|
-
|
|
151
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
152
|
-
class OrderRejected(BrokerResponseEvent):
|
|
153
|
-
order_id: uuid.UUID
|
|
154
|
-
reason: Models.RejectionReason
|
|
155
|
-
|
|
156
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
157
|
-
class OrderCancelled(BrokerResponseEvent):
|
|
158
|
-
order_id: uuid.UUID
|
|
159
|
-
|
|
160
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
161
|
-
class OrderExpired(BrokerResponseEvent):
|
|
162
|
-
order_id: uuid.UUID
|
|
163
|
-
|
|
164
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
165
|
-
class CancelRejected(BrokerResponseEvent):
|
|
166
|
-
order_id: uuid.UUID
|
|
167
|
-
reason: Models.RejectionReason
|
|
168
|
-
|
|
169
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
170
|
-
class ModifyRejected(BrokerResponseEvent):
|
|
171
|
-
order_id: uuid.UUID
|
|
172
|
-
reason: Models.RejectionReason
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
class BaseConsumer(abc.ABC):
|
|
176
|
-
"""
|
|
177
|
-
Base class for all consumers.
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
def __init__(self) -> None:
|
|
181
|
-
self._queue: queue.Queue[Events.BaseEvent] = queue.Queue()
|
|
182
|
-
self._thread = threading.Thread(
|
|
183
|
-
target=self._consume, name=self.__class__.__name__, daemon=True
|
|
184
|
-
)
|
|
185
|
-
self._thread.start()
|
|
186
|
-
|
|
187
|
-
@abc.abstractmethod
|
|
188
|
-
def on_event(self, event: Events.BaseEvent) -> None:
|
|
189
|
-
pass
|
|
190
|
-
|
|
191
|
-
def receive(self, event: Events.BaseEvent) -> None:
|
|
192
|
-
self._queue.put(event)
|
|
193
|
-
|
|
194
|
-
def _consume(self) -> None:
|
|
195
|
-
while True:
|
|
196
|
-
event = self._queue.get()
|
|
197
|
-
if isinstance(event, Events.SystemShutdown):
|
|
198
|
-
break
|
|
199
|
-
self.on_event(event)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
class EventBus:
|
|
203
|
-
"""
|
|
204
|
-
Event bus for publishing events to the consumers subscribed to them.
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
def __init__(self) -> None:
|
|
208
|
-
self._subscriptions: defaultdict[type[Events.BaseEvent], list[BaseConsumer]] = (
|
|
209
|
-
defaultdict(list)
|
|
210
|
-
)
|
|
211
|
-
self._lock: threading.Lock = threading.Lock()
|
|
212
|
-
|
|
213
|
-
def subscribe(self, subscriber: BaseConsumer, event_type: type[Events.BaseEvent]):
|
|
214
|
-
with self._lock:
|
|
215
|
-
if subscriber not in self._subscriptions[event_type]:
|
|
216
|
-
self._subscriptions[event_type].append(subscriber)
|
|
217
|
-
|
|
218
|
-
def unsubscribe(self, subscriber: BaseConsumer):
|
|
219
|
-
with self._lock:
|
|
220
|
-
for consumer_list in self._subscriptions.values():
|
|
221
|
-
if subscriber in consumer_list:
|
|
222
|
-
consumer_list.remove(subscriber)
|
|
223
|
-
|
|
224
|
-
def publish(self, event: Events.BaseEvent) -> None:
|
|
225
|
-
with self._lock:
|
|
226
|
-
consumers = list(self._subscriptions[type(event)])
|
|
227
|
-
for consumer in consumers:
|
|
228
|
-
consumer.receive(event)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
event_bus = EventBus()
|
|
232
|
-
"""
|
|
233
|
-
Global event bus instance.
|
|
234
|
-
"""
|
|
@@ -1,172 +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
|
-
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|