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.
Files changed (31) hide show
  1. {onesecondtrader-0.22.0 → onesecondtrader-0.24.0}/PKG-INFO +1 -1
  2. {onesecondtrader-0.22.0 → onesecondtrader-0.24.0}/pyproject.toml +1 -1
  3. onesecondtrader-0.24.0/src/onesecondtrader/analyst/__init__.py +0 -0
  4. onesecondtrader-0.24.0/src/onesecondtrader/analyst/charting.py +0 -0
  5. onesecondtrader-0.24.0/src/onesecondtrader/components/__init__.py +0 -0
  6. onesecondtrader-0.24.0/src/onesecondtrader/components/broker_base.py +0 -0
  7. onesecondtrader-0.24.0/src/onesecondtrader/components/datafeed_base.py +0 -0
  8. onesecondtrader-0.24.0/src/onesecondtrader/components/indicator_base.py +0 -0
  9. onesecondtrader-0.24.0/src/onesecondtrader/components/strategy_base.py +0 -0
  10. onesecondtrader-0.24.0/src/onesecondtrader/connectors/__init__.py +0 -0
  11. onesecondtrader-0.24.0/src/onesecondtrader/connectors/ib.py +0 -0
  12. onesecondtrader-0.24.0/src/onesecondtrader/connectors/mt5.py +0 -0
  13. onesecondtrader-0.24.0/src/onesecondtrader/connectors/simulated.py +0 -0
  14. onesecondtrader-0.24.0/src/onesecondtrader/core/__init__.py +47 -0
  15. onesecondtrader-0.24.0/src/onesecondtrader/core/domain_models.py +30 -0
  16. onesecondtrader-0.24.0/src/onesecondtrader/core/event_bus.py +60 -0
  17. onesecondtrader-0.24.0/src/onesecondtrader/core/event_messages.py +128 -0
  18. onesecondtrader-0.24.0/src/onesecondtrader/core/event_publisher.py +23 -0
  19. onesecondtrader-0.24.0/src/onesecondtrader/core/event_subscriber.py +76 -0
  20. onesecondtrader-0.24.0/src/onesecondtrader/libraries/__init__.py +0 -0
  21. onesecondtrader-0.24.0/src/onesecondtrader/libraries/indicators.py +0 -0
  22. onesecondtrader-0.24.0/src/onesecondtrader/libraries/strategies.py +0 -0
  23. onesecondtrader-0.24.0/src/onesecondtrader/observers/__init__.py +0 -0
  24. onesecondtrader-0.24.0/src/onesecondtrader/observers/csvbookkeeper.py +0 -0
  25. onesecondtrader-0.22.0/src/onesecondtrader/brokers.py +0 -92
  26. onesecondtrader-0.22.0/src/onesecondtrader/core.py +0 -260
  27. onesecondtrader-0.22.0/src/onesecondtrader/datafeeds.py +0 -173
  28. onesecondtrader-0.22.0/src/onesecondtrader/indicators.py +0 -106
  29. {onesecondtrader-0.22.0 → onesecondtrader-0.24.0}/LICENSE +0 -0
  30. {onesecondtrader-0.22.0 → onesecondtrader-0.24.0}/README.md +0 -0
  31. {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.22.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.22.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"}
@@ -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: ...
@@ -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