onesecondtrader 0.22.0__tar.gz → 0.24.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.22.0 → onesecondtrader-0.24.0}/PKG-INFO +1 -1
- {onesecondtrader-0.22.0 → onesecondtrader-0.24.0}/pyproject.toml +1 -1
- onesecondtrader-0.24.0/src/onesecondtrader/analyst/__init__.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/analyst/charting.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/components/__init__.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/components/broker_base.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/components/datafeed_base.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/components/indicator_base.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/components/strategy_base.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/connectors/__init__.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/connectors/ib.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/connectors/mt5.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/connectors/simulated.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/core/__init__.py +47 -0
- onesecondtrader-0.24.0/src/onesecondtrader/core/domain_models.py +30 -0
- onesecondtrader-0.24.0/src/onesecondtrader/core/event_bus.py +60 -0
- onesecondtrader-0.24.0/src/onesecondtrader/core/event_messages.py +128 -0
- onesecondtrader-0.24.0/src/onesecondtrader/core/event_publisher.py +23 -0
- onesecondtrader-0.24.0/src/onesecondtrader/core/event_subscriber.py +76 -0
- onesecondtrader-0.24.0/src/onesecondtrader/libraries/__init__.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/libraries/indicators.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/libraries/strategies.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/observers/__init__.py +0 -0
- onesecondtrader-0.24.0/src/onesecondtrader/observers/csvbookkeeper.py +0 -0
- onesecondtrader-0.22.0/src/onesecondtrader/brokers.py +0 -92
- onesecondtrader-0.22.0/src/onesecondtrader/core.py +0 -260
- onesecondtrader-0.22.0/src/onesecondtrader/datafeeds.py +0 -173
- onesecondtrader-0.22.0/src/onesecondtrader/indicators.py +0 -106
- {onesecondtrader-0.22.0 → onesecondtrader-0.24.0}/LICENSE +0 -0
- {onesecondtrader-0.22.0 → onesecondtrader-0.24.0}/README.md +0 -0
- {onesecondtrader-0.22.0 → onesecondtrader-0.24.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.24.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.24.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from .domain_models import BarPeriod, OrderSide, OrderType
|
|
2
|
+
from .event_bus import EventBus
|
|
3
|
+
from .event_messages import (
|
|
4
|
+
AcceptedOrderCancellation,
|
|
5
|
+
AcceptedOrderModification,
|
|
6
|
+
AcceptedOrderSubmission,
|
|
7
|
+
BrokerRequestEventBase,
|
|
8
|
+
BrokerResponseEventBase,
|
|
9
|
+
ConfirmedOrderExpired,
|
|
10
|
+
ConfirmedOrderFilled,
|
|
11
|
+
EventBase,
|
|
12
|
+
MarketEventBase,
|
|
13
|
+
NewBar,
|
|
14
|
+
RejectedOrderCancellation,
|
|
15
|
+
RejectedOrderModification,
|
|
16
|
+
RejectedOrderSubmission,
|
|
17
|
+
RequestOrderCancellation,
|
|
18
|
+
RequestOrderModification,
|
|
19
|
+
RequestOrderSubmission,
|
|
20
|
+
)
|
|
21
|
+
from .event_publisher import EventPublisher
|
|
22
|
+
from .event_subscriber import EventSubscriber
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BarPeriod",
|
|
26
|
+
"OrderSide",
|
|
27
|
+
"OrderType",
|
|
28
|
+
"EventBus",
|
|
29
|
+
"AcceptedOrderCancellation",
|
|
30
|
+
"AcceptedOrderModification",
|
|
31
|
+
"AcceptedOrderSubmission",
|
|
32
|
+
"BrokerRequestEventBase",
|
|
33
|
+
"BrokerResponseEventBase",
|
|
34
|
+
"ConfirmedOrderExpired",
|
|
35
|
+
"ConfirmedOrderFilled",
|
|
36
|
+
"EventBase",
|
|
37
|
+
"MarketEventBase",
|
|
38
|
+
"NewBar",
|
|
39
|
+
"RejectedOrderCancellation",
|
|
40
|
+
"RejectedOrderModification",
|
|
41
|
+
"RejectedOrderSubmission",
|
|
42
|
+
"RequestOrderCancellation",
|
|
43
|
+
"RequestOrderModification",
|
|
44
|
+
"RequestOrderSubmission",
|
|
45
|
+
"EventPublisher",
|
|
46
|
+
"EventSubscriber",
|
|
47
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
---
|
|
3
|
+
This module defines the core domain models used throughout the system.
|
|
4
|
+
|
|
5
|
+
Domain models are enumerations that define the shared vocabulary of the trading system.
|
|
6
|
+
They provide a fixed set of valid values for core concepts and are used across all
|
|
7
|
+
system components to ensure consistency and type safety.
|
|
8
|
+
---
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OrderType(enum.Enum):
|
|
15
|
+
LIMIT = enum.auto()
|
|
16
|
+
MARKET = enum.auto()
|
|
17
|
+
STOP = enum.auto()
|
|
18
|
+
STOP_LIMIT = enum.auto()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OrderSide(enum.Enum):
|
|
22
|
+
BUY = enum.auto()
|
|
23
|
+
SELL = enum.auto()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BarPeriod(enum.Enum):
|
|
27
|
+
SECOND = 32
|
|
28
|
+
MINUTE = 33
|
|
29
|
+
HOUR = 34
|
|
30
|
+
DAY = 35
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read first: [Event Messages](./event_messages.md).
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
This module defines the central event bus for publish-subscribe communication.
|
|
6
|
+
|
|
7
|
+
EventBus is the core messaging infrastructure that routes events between system
|
|
8
|
+
components.
|
|
9
|
+
Subscribers register interest in specific event types and receive events when publishers
|
|
10
|
+
dispatch them.
|
|
11
|
+
Event routing uses exact type matching, so subscribing to a base class will not receive
|
|
12
|
+
events of derived types.
|
|
13
|
+
---
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import collections
|
|
17
|
+
import threading
|
|
18
|
+
import typing
|
|
19
|
+
|
|
20
|
+
from .event_messages import EventBase
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@typing.runtime_checkable
|
|
24
|
+
class EventSubscriberLike(typing.Protocol):
|
|
25
|
+
def receive(self, event: EventBase) -> None: ...
|
|
26
|
+
def wait_until_idle(self) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EventBus:
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self._per_event_subscriptions: collections.defaultdict[
|
|
32
|
+
type[EventBase], set[EventSubscriberLike]
|
|
33
|
+
] = collections.defaultdict(set)
|
|
34
|
+
self._subscribers: set[EventSubscriberLike] = set()
|
|
35
|
+
self._lock: threading.Lock = threading.Lock()
|
|
36
|
+
|
|
37
|
+
def subscribe(
|
|
38
|
+
self, subscriber: EventSubscriberLike, event_type: type[EventBase]
|
|
39
|
+
) -> None:
|
|
40
|
+
with self._lock:
|
|
41
|
+
self._subscribers.add(subscriber)
|
|
42
|
+
self._per_event_subscriptions[event_type].add(subscriber)
|
|
43
|
+
|
|
44
|
+
def unsubscribe(self, subscriber: EventSubscriberLike) -> None:
|
|
45
|
+
with self._lock:
|
|
46
|
+
for set_of_event_subscribers in self._per_event_subscriptions.values():
|
|
47
|
+
set_of_event_subscribers.discard(subscriber)
|
|
48
|
+
self._subscribers.discard(subscriber)
|
|
49
|
+
|
|
50
|
+
def publish(self, event: EventBase) -> None:
|
|
51
|
+
with self._lock:
|
|
52
|
+
subscribers = self._per_event_subscriptions[type(event)].copy()
|
|
53
|
+
for subscriber in subscribers:
|
|
54
|
+
subscriber.receive(event)
|
|
55
|
+
|
|
56
|
+
def wait_until_system_idle(self) -> None:
|
|
57
|
+
with self._lock:
|
|
58
|
+
subscribers = self._subscribers.copy()
|
|
59
|
+
for subscriber in subscribers:
|
|
60
|
+
subscriber.wait_until_idle()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read first: [Domain Models](./domain_models.md).
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
This module defines the event messages used throughout the system.
|
|
6
|
+
|
|
7
|
+
Event messages are immutable dataclasses used for communication between system
|
|
8
|
+
components
|
|
9
|
+
They are semantically grouped into market, broker request, and broker
|
|
10
|
+
response events via inheritance from dedicated base classes.
|
|
11
|
+
---
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
from .domain_models import BarPeriod, OrderSide, OrderType
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
22
|
+
class EventBase:
|
|
23
|
+
ts_event: pd.Timestamp
|
|
24
|
+
ts_created: pd.Timestamp = dataclasses.field(
|
|
25
|
+
default_factory=lambda: pd.Timestamp.now(tz="UTC")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
30
|
+
class MarketEventBase(EventBase):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
35
|
+
class BrokerRequestEventBase(EventBase):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
40
|
+
class BrokerResponseEventBase(EventBase):
|
|
41
|
+
ts_broker: pd.Timestamp
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
45
|
+
class NewBar(MarketEventBase):
|
|
46
|
+
symbol: str
|
|
47
|
+
bar_period: BarPeriod
|
|
48
|
+
open: float
|
|
49
|
+
high: float
|
|
50
|
+
low: float
|
|
51
|
+
close: float
|
|
52
|
+
volume: int | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
56
|
+
class RequestOrderSubmission(BrokerRequestEventBase):
|
|
57
|
+
order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
58
|
+
symbol: str
|
|
59
|
+
order_type: OrderType
|
|
60
|
+
side: OrderSide
|
|
61
|
+
quantity: float
|
|
62
|
+
limit_price: float | None = None
|
|
63
|
+
stop_price: float | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
67
|
+
class RequestOrderCancellation(BrokerRequestEventBase):
|
|
68
|
+
symbol: str
|
|
69
|
+
order_id: uuid.UUID
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
73
|
+
class RequestOrderModification(BrokerRequestEventBase):
|
|
74
|
+
symbol: str
|
|
75
|
+
order_id: uuid.UUID
|
|
76
|
+
quantity: float | None = None
|
|
77
|
+
limit_price: float | None = None
|
|
78
|
+
stop_price: float | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
82
|
+
class AcceptedOrderSubmission(BrokerResponseEventBase):
|
|
83
|
+
order_id: uuid.UUID
|
|
84
|
+
broker_order_id: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
88
|
+
class AcceptedOrderModification(BrokerResponseEventBase):
|
|
89
|
+
order_id: uuid.UUID
|
|
90
|
+
broker_order_id: str | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
94
|
+
class AcceptedOrderCancellation(BrokerResponseEventBase):
|
|
95
|
+
order_id: uuid.UUID
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
99
|
+
class RejectedOrderSubmission(BrokerResponseEventBase):
|
|
100
|
+
order_id: uuid.UUID
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
104
|
+
class RejectedOrderModification(BrokerResponseEventBase):
|
|
105
|
+
order_id: uuid.UUID
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
109
|
+
class RejectedOrderCancellation(BrokerResponseEventBase):
|
|
110
|
+
order_id: uuid.UUID
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
114
|
+
class ConfirmedOrderFilled(BrokerResponseEventBase):
|
|
115
|
+
fill_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
116
|
+
broker_fill_id: str | None = None
|
|
117
|
+
associated_order_id: uuid.UUID
|
|
118
|
+
symbol: str
|
|
119
|
+
side: OrderSide
|
|
120
|
+
quantity_filled: float
|
|
121
|
+
fill_price: float
|
|
122
|
+
commission: float
|
|
123
|
+
exchange: str = "SIMULATED"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
127
|
+
class ConfirmedOrderExpired(BrokerResponseEventBase):
|
|
128
|
+
order_id: uuid.UUID
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read first: [Event Bus](./event_bus.md), [Event Messages](./event_messages.md).
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
This module defines the base class for components that publish events.
|
|
6
|
+
|
|
7
|
+
EventPublisher provides a minimal interface for publishing events to the event bus.
|
|
8
|
+
Components that only publish events inherit from this class directly.
|
|
9
|
+
Components that both subscribe and publish inherit from both EventSubscriber and
|
|
10
|
+
EventPublisher.
|
|
11
|
+
---
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .event_bus import EventBus
|
|
15
|
+
from .event_messages import EventBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EventPublisher:
|
|
19
|
+
def __init__(self, event_bus: EventBus) -> None:
|
|
20
|
+
self._event_bus = event_bus
|
|
21
|
+
|
|
22
|
+
def _publish(self, event: EventBase) -> None:
|
|
23
|
+
self._event_bus.publish(event)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read first: [Event Bus](./event_bus.md), [Event Messages](./event_messages.md).
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
This module defines the base class for components that subscribe to events.
|
|
6
|
+
|
|
7
|
+
EventSubscriber provides a threaded event loop that receives events from the event bus.
|
|
8
|
+
Each subscriber runs in its own thread and processes events from its internal queue.
|
|
9
|
+
Components that only subscribe inherit from this class directly.
|
|
10
|
+
Components that both subscribe and publish inherit from both EventSubscriber and
|
|
11
|
+
EventPublisher.
|
|
12
|
+
---
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import abc
|
|
16
|
+
import queue
|
|
17
|
+
import threading
|
|
18
|
+
|
|
19
|
+
from .event_bus import EventBus
|
|
20
|
+
from .event_messages import EventBase
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EventSubscriber(abc.ABC):
|
|
24
|
+
def __init__(self, event_bus: EventBus) -> None:
|
|
25
|
+
self._event_bus = event_bus
|
|
26
|
+
self._queue: queue.Queue[EventBase | None] = queue.Queue()
|
|
27
|
+
self._running = True
|
|
28
|
+
self._thread = threading.Thread(
|
|
29
|
+
target=self._event_loop, name=self.__class__.__name__
|
|
30
|
+
)
|
|
31
|
+
self._thread.start()
|
|
32
|
+
|
|
33
|
+
def receive(self, event: EventBase) -> None:
|
|
34
|
+
if self._running:
|
|
35
|
+
self._queue.put(event)
|
|
36
|
+
|
|
37
|
+
def wait_until_idle(self) -> None:
|
|
38
|
+
if not self._running:
|
|
39
|
+
return
|
|
40
|
+
self._queue.join()
|
|
41
|
+
|
|
42
|
+
def shutdown(self) -> None:
|
|
43
|
+
if not self._running:
|
|
44
|
+
return
|
|
45
|
+
self._running = False
|
|
46
|
+
self._event_bus.unsubscribe(self)
|
|
47
|
+
self._queue.put(None)
|
|
48
|
+
if threading.current_thread() is not self._thread:
|
|
49
|
+
self._thread.join()
|
|
50
|
+
|
|
51
|
+
def _subscribe(self, *event_types: type[EventBase]) -> None:
|
|
52
|
+
for event_type in event_types:
|
|
53
|
+
self._event_bus.subscribe(self, event_type)
|
|
54
|
+
|
|
55
|
+
def _event_loop(self) -> None:
|
|
56
|
+
while True:
|
|
57
|
+
event = self._queue.get()
|
|
58
|
+
if event is None:
|
|
59
|
+
self._queue.task_done()
|
|
60
|
+
break
|
|
61
|
+
try:
|
|
62
|
+
self._on_event(event)
|
|
63
|
+
except Exception:
|
|
64
|
+
self._on_exception()
|
|
65
|
+
finally:
|
|
66
|
+
self._queue.task_done()
|
|
67
|
+
self._cleanup()
|
|
68
|
+
|
|
69
|
+
def _on_exception(self) -> None:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def _cleanup(self) -> None:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
@abc.abstractmethod
|
|
76
|
+
def _on_event(self, event: EventBase) -> None: ...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
|
@@ -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
|
-
"""
|
|
@@ -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
|
-
"""
|
|
@@ -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
|