onesecondtrader 0.40.0__tar.gz → 0.43.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 (52) hide show
  1. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/PKG-INFO +3 -1
  2. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/pyproject.toml +4 -1
  3. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/__init__.py +4 -0
  4. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/datafeeds/simulated.py +63 -17
  5. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/datafeeds/base.py +3 -0
  6. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/events/requests.py +2 -0
  7. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/models/__init__.py +4 -1
  8. onesecondtrader-0.43.0/src/onesecondtrader/core/models/orders.py +27 -0
  9. onesecondtrader-0.43.0/src/onesecondtrader/core/models/params.py +21 -0
  10. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/models/records.py +2 -0
  11. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/strategies/base.py +17 -4
  12. onesecondtrader-0.43.0/src/onesecondtrader/core/strategies/examples.py +47 -0
  13. onesecondtrader-0.43.0/src/onesecondtrader/dashboard/__init__.py +3 -0
  14. onesecondtrader-0.43.0/src/onesecondtrader/dashboard/app.py +2972 -0
  15. onesecondtrader-0.43.0/src/onesecondtrader/dashboard/registry.py +100 -0
  16. onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/__init__.py +7 -0
  17. onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/orchestrator.py +105 -0
  18. onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/recorder.py +199 -0
  19. onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/schema.sql +212 -0
  20. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/secmaster/schema.sql +48 -0
  21. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/secmaster/utils.py +90 -0
  22. onesecondtrader-0.40.0/src/onesecondtrader/core/models/orders.py +0 -15
  23. onesecondtrader-0.40.0/src/onesecondtrader/core/strategies/examples.py +0 -35
  24. onesecondtrader-0.40.0/src/onesecondtrader/dashboard/__init__.py +0 -0
  25. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/LICENSE +0 -0
  26. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/README.md +0 -0
  27. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/__init__.py +0 -0
  28. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/brokers/__init__.py +0 -0
  29. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/brokers/ib.py +0 -0
  30. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/brokers/simulated.py +0 -0
  31. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/datafeeds/__init__.py +0 -0
  32. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/datafeeds/ib.py +0 -0
  33. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/gateways/__init__.py +0 -0
  34. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/connectors/gateways/ib.py +0 -0
  35. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/__init__.py +0 -0
  36. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/brokers/__init__.py +0 -0
  37. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/brokers/base.py +0 -0
  38. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/datafeeds/__init__.py +0 -0
  39. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/events/__init__.py +0 -0
  40. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/events/bases.py +0 -0
  41. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/events/market.py +0 -0
  42. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/events/responses.py +0 -0
  43. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/indicators/__init__.py +0 -0
  44. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/indicators/averages.py +0 -0
  45. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/indicators/bar.py +0 -0
  46. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/indicators/base.py +0 -0
  47. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/messaging/__init__.py +0 -0
  48. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/messaging/eventbus.py +0 -0
  49. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/messaging/subscriber.py +0 -0
  50. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/models/data.py +0 -0
  51. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/core/strategies/__init__.py +0 -0
  52. {onesecondtrader-0.40.0 → onesecondtrader-0.43.0}/src/onesecondtrader/secmaster/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.40.0
3
+ Version: 0.43.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
@@ -12,12 +12,14 @@ Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
14
  Requires-Dist: databento (>=0.69.0,<0.70.0)
15
+ Requires-Dist: fastapi (>=0.128.0,<0.129.0)
15
16
  Requires-Dist: ib-async (>=2.1.0,<3.0.0)
16
17
  Requires-Dist: matplotlib (>=3.10.7,<4.0.0)
17
18
  Requires-Dist: mplfinance (>=0.12.10b0,<0.13.0)
18
19
  Requires-Dist: pandas (>=2.3.1,<3.0.0)
19
20
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
20
21
  Requires-Dist: tqdm (>=4.67.1,<5.0.0)
22
+ Requires-Dist: uvicorn (>=0.40.0,<0.41.0)
21
23
  Description-Content-Type: text/markdown
22
24
 
23
25
  # OneSecondTrader
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "onesecondtrader"
3
- version = "0.40.0"
3
+ version = "0.43.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"}
@@ -15,6 +15,8 @@ dependencies = [
15
15
  "tqdm (>=4.67.1,<5.0.0)",
16
16
  "mplfinance (>=0.12.10b0,<0.13.0)",
17
17
  "ib-async (>=2.1.0,<3.0.0)",
18
+ "fastapi (>=0.128.0,<0.129.0)",
19
+ "uvicorn (>=0.40.0,<0.41.0)",
18
20
  ]
19
21
 
20
22
  [tool.poetry]
@@ -62,6 +64,7 @@ build-backend = "poetry.core.masonry.api"
62
64
  explicit_package_bases = true
63
65
  mypy_path = "src"
64
66
  namespace_packages = true
67
+ exclude = ["ignore/"]
65
68
 
66
69
  [tool.pytest.ini_options]
67
70
  testpaths = ["tests"]
@@ -1,4 +1,5 @@
1
1
  __all__ = [
2
+ "ActionType",
2
3
  "BarPeriod",
3
4
  "BarProcessed",
4
5
  "BarReceived",
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "OrderSide",
19
20
  "OrderSubmission",
20
21
  "OrderType",
22
+ "ParamSpec",
21
23
  "SimulatedBroker",
22
24
  "SimulatedDatafeed",
23
25
  "SimpleMovingAverage",
@@ -46,11 +48,13 @@ from onesecondtrader.core.indicators import (
46
48
  Volume,
47
49
  )
48
50
  from onesecondtrader.core.models import (
51
+ ActionType,
49
52
  BarPeriod,
50
53
  FillRecord,
51
54
  InputSource,
52
55
  OrderRecord,
53
56
  OrderSide,
54
57
  OrderType,
58
+ ParamSpec,
55
59
  )
56
60
  from onesecondtrader.core.strategies import SMACrossover, StrategyBase
@@ -21,6 +21,8 @@ _PRICE_SCALE = 1e9
21
21
 
22
22
  class SimulatedDatafeed(DatafeedBase):
23
23
  db_path: str = ""
24
+ start_ts: int | None = None
25
+ end_ts: int | None = None
24
26
 
25
27
  def __init__(self, event_bus: messaging.EventBus) -> None:
26
28
  super().__init__(event_bus)
@@ -51,9 +53,14 @@ class SimulatedDatafeed(DatafeedBase):
51
53
  self._connected = False
52
54
 
53
55
  def subscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
54
- if (symbol, bar_period) in self._subscriptions:
55
- return
56
56
  self._subscriptions.add((symbol, bar_period))
57
+
58
+ def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
59
+ self._subscriptions.discard((symbol, bar_period))
60
+
61
+ def wait_until_complete(self) -> None:
62
+ if not self._subscriptions:
63
+ return
57
64
  if self._thread is None or not self._thread.is_alive():
58
65
  self._stop_event.clear()
59
66
  self._thread = threading.Thread(
@@ -62,9 +69,7 @@ class SimulatedDatafeed(DatafeedBase):
62
69
  daemon=False,
63
70
  )
64
71
  self._thread.start()
65
-
66
- def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
67
- self._subscriptions.discard((symbol, bar_period))
72
+ self._thread.join()
68
73
 
69
74
  def _stream(self) -> None:
70
75
  if not self._connection:
@@ -83,26 +88,40 @@ class SimulatedDatafeed(DatafeedBase):
83
88
 
84
89
  cursor = self._connection.cursor()
85
90
 
86
- instrument_ids = self._resolve_instrument_ids(cursor, symbols)
87
- if not instrument_ids:
91
+ symbology_map = self._load_symbology(cursor, symbols)
92
+ if not symbology_map:
88
93
  return
89
94
 
90
- instrument_to_symbol = {v: k for k, v in instrument_ids.items()}
91
- id_list = list(instrument_ids.values())
95
+ all_instrument_ids = set()
96
+ for entries in symbology_map.values():
97
+ for entry in entries:
98
+ all_instrument_ids.add(entry["instrument_id"])
99
+
100
+ id_list = list(all_instrument_ids)
92
101
  rtype_list = list(set(rtype_by_symbol.values()))
93
102
 
94
103
  placeholders_ids = ",".join("?" * len(id_list))
95
104
  placeholders_rtypes = ",".join("?" * len(rtype_list))
96
105
 
106
+ date_filter = ""
107
+ params = id_list + rtype_list
108
+ if self.start_ts is not None:
109
+ date_filter += " AND ts_event >= ?"
110
+ params.append(self.start_ts)
111
+ if self.end_ts is not None:
112
+ date_filter += " AND ts_event <= ?"
113
+ params.append(self.end_ts)
114
+
97
115
  query = f"""
98
116
  SELECT instrument_id, rtype, ts_event, open, high, low, close, volume
99
117
  FROM ohlcv
100
118
  WHERE instrument_id IN ({placeholders_ids})
101
119
  AND rtype IN ({placeholders_rtypes})
120
+ {date_filter}
102
121
  ORDER BY ts_event
103
122
  """
104
123
 
105
- cursor.execute(query, id_list + rtype_list)
124
+ cursor.execute(query, params)
106
125
 
107
126
  while True:
108
127
  if self._stop_event.is_set():
@@ -113,7 +132,10 @@ class SimulatedDatafeed(DatafeedBase):
113
132
  break
114
133
 
115
134
  instrument_id, rtype, ts_event, open_, high, low, close, volume = row
116
- symbol = instrument_to_symbol.get(instrument_id)
135
+
136
+ symbol = self._resolve_symbol_for_bar(
137
+ symbology_map, instrument_id, ts_event
138
+ )
117
139
  if symbol is None:
118
140
  continue
119
141
 
@@ -137,16 +159,40 @@ class SimulatedDatafeed(DatafeedBase):
137
159
  )
138
160
  self._event_bus.wait_until_system_idle()
139
161
 
140
- def _resolve_instrument_ids(
162
+ def _load_symbology(
141
163
  self, cursor: sqlite3.Cursor, symbols: list[str]
142
- ) -> dict[str, int]:
164
+ ) -> dict[str, list[dict]]:
143
165
  placeholders = ",".join("?" * len(symbols))
144
166
  query = f"""
145
- SELECT symbol, instrument_id
167
+ SELECT symbol, instrument_id, start_date, end_date
146
168
  FROM symbology
147
169
  WHERE symbol IN ({placeholders})
148
- GROUP BY symbol
149
- HAVING start_date = MAX(start_date)
170
+ ORDER BY symbol, start_date
150
171
  """
151
172
  cursor.execute(query, symbols)
152
- return {row[0]: row[1] for row in cursor.fetchall()}
173
+
174
+ result: dict[str, list[dict]] = {}
175
+ for row in cursor.fetchall():
176
+ symbol, instrument_id, start_date, end_date = row
177
+ start_ns = int(pd.Timestamp(start_date, tz="UTC").value)
178
+ end_ns = int(pd.Timestamp(end_date, tz="UTC").value)
179
+ if symbol not in result:
180
+ result[symbol] = []
181
+ result[symbol].append(
182
+ {
183
+ "instrument_id": instrument_id,
184
+ "start_ns": start_ns,
185
+ "end_ns": end_ns,
186
+ }
187
+ )
188
+ return result
189
+
190
+ def _resolve_symbol_for_bar(
191
+ self, symbology_map: dict[str, list[dict]], instrument_id: int, ts_event: int
192
+ ) -> str | None:
193
+ for symbol, entries in symbology_map.items():
194
+ for entry in entries:
195
+ if entry["instrument_id"] == instrument_id:
196
+ if entry["start_ns"] <= ts_event < entry["end_ns"]:
197
+ return symbol
198
+ return None
@@ -27,3 +27,6 @@ class DatafeedBase(abc.ABC):
27
27
  @abc.abstractmethod
28
28
  def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
29
29
  pass
30
+
31
+ def wait_until_complete(self) -> None:
32
+ pass
@@ -16,6 +16,8 @@ class OrderSubmission(bases.BrokerRequestEvent):
16
16
  quantity: float
17
17
  limit_price: float | None = None
18
18
  stop_price: float | None = None
19
+ action: models.orders.ActionType | None = None
20
+ signal: str | None = None
19
21
 
20
22
 
21
23
  @dataclasses.dataclass(kw_only=True, frozen=True)
@@ -1,12 +1,15 @@
1
1
  __all__ = [
2
+ "ActionType",
2
3
  "BarPeriod",
3
4
  "InputSource",
4
5
  "OrderSide",
5
6
  "OrderType",
6
7
  "OrderRecord",
7
8
  "FillRecord",
9
+ "ParamSpec",
8
10
  ]
9
11
 
10
12
  from .data import BarPeriod, InputSource
11
- from .orders import OrderSide, OrderType
13
+ from .orders import ActionType, OrderSide, OrderType
12
14
  from .records import OrderRecord, FillRecord
15
+ from .params import ParamSpec
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+
6
+ class OrderType(enum.Enum):
7
+ LIMIT = enum.auto()
8
+ MARKET = enum.auto()
9
+ STOP = enum.auto()
10
+ STOP_LIMIT = enum.auto()
11
+
12
+
13
+ class OrderSide(enum.Enum):
14
+ BUY = enum.auto()
15
+ SELL = enum.auto()
16
+
17
+
18
+ class ActionType(enum.Enum):
19
+ ENTRY = enum.auto()
20
+ ENTRY_LONG = enum.auto()
21
+ ENTRY_SHORT = enum.auto()
22
+ EXIT = enum.auto()
23
+ EXIT_LONG = enum.auto()
24
+ EXIT_SHORT = enum.auto()
25
+ ADD = enum.auto()
26
+ REDUCE = enum.auto()
27
+ REVERSE = enum.auto()
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import enum
5
+
6
+
7
+ @dataclasses.dataclass
8
+ class ParamSpec:
9
+ default: int | float | str | bool | enum.Enum
10
+ min: int | float | None = None
11
+ max: int | float | None = None
12
+ step: int | float | None = None
13
+ choices: list | None = None
14
+
15
+ @property
16
+ def resolved_choices(self) -> list | None:
17
+ if self.choices is not None:
18
+ return self.choices
19
+ if isinstance(self.default, enum.Enum):
20
+ return list(type(self.default))
21
+ return None
@@ -17,6 +17,8 @@ class OrderRecord:
17
17
  quantity: float
18
18
  limit_price: float | None = None
19
19
  stop_price: float | None = None
20
+ action: orders.ActionType | None = None
21
+ signal: str | None = None
20
22
  filled_quantity: float = 0.0
21
23
 
22
24
 
@@ -10,11 +10,17 @@ from onesecondtrader.core import events, indicators, messaging, models
10
10
 
11
11
 
12
12
  class StrategyBase(messaging.Subscriber, abc.ABC):
13
+ name: str = ""
13
14
  symbols: list[str] = []
14
- bar_period: models.BarPeriod = models.BarPeriod.SECOND
15
+ parameters: dict[str, models.ParamSpec] = {}
15
16
 
16
- def __init__(self, event_bus: messaging.EventBus) -> None:
17
+ def __init__(self, event_bus: messaging.EventBus, **overrides) -> None:
17
18
  super().__init__(event_bus)
19
+
20
+ for name, spec in self.parameters.items():
21
+ value = overrides.get(name, spec.default)
22
+ setattr(self, name, value)
23
+
18
24
  self._subscribe(
19
25
  events.BarReceived,
20
26
  events.OrderSubmissionAccepted,
@@ -70,8 +76,9 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
70
76
  quantity: float,
71
77
  limit_price: float | None = None,
72
78
  stop_price: float | None = None,
79
+ action: models.ActionType | None = None,
80
+ signal: str | None = None,
73
81
  ) -> uuid.UUID:
74
- # Uses bar timestamp for backtest compatibility; ts_created tracks real wall-clock time
75
82
  order_id = uuid.uuid4()
76
83
 
77
84
  event = events.OrderSubmission(
@@ -83,6 +90,8 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
83
90
  quantity=quantity,
84
91
  limit_price=limit_price,
85
92
  stop_price=stop_price,
93
+ action=action,
94
+ signal=signal,
86
95
  )
87
96
 
88
97
  order = models.OrderRecord(
@@ -93,6 +102,8 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
93
102
  quantity=quantity,
94
103
  limit_price=limit_price,
95
104
  stop_price=stop_price,
105
+ action=action,
106
+ signal=signal,
96
107
  )
97
108
 
98
109
  self._submitted_orders[order_id] = order
@@ -131,6 +142,8 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
131
142
  stop_price=(
132
143
  stop_price if stop_price is not None else original_order.stop_price
133
144
  ),
145
+ action=original_order.action,
146
+ signal=original_order.signal,
134
147
  filled_quantity=original_order.filled_quantity,
135
148
  )
136
149
 
@@ -179,7 +192,7 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
179
192
  def _on_bar_received(self, event: events.BarReceived) -> None:
180
193
  if event.symbol not in self.symbols:
181
194
  return
182
- if event.bar_period != self.bar_period:
195
+ if event.bar_period != self.bar_period: # type: ignore[attr-defined]
183
196
  return
184
197
 
185
198
  self._current_symbol = event.symbol
@@ -0,0 +1,47 @@
1
+ from onesecondtrader.core import events, indicators, models
2
+ from .base import StrategyBase
3
+
4
+
5
+ class SMACrossover(StrategyBase):
6
+ name = "SMA Crossover"
7
+ parameters = {
8
+ "bar_period": models.ParamSpec(default=models.BarPeriod.SECOND),
9
+ "fast_period": models.ParamSpec(default=20, min=5, max=100, step=1),
10
+ "slow_period": models.ParamSpec(default=100, min=10, max=500, step=1),
11
+ "quantity": models.ParamSpec(default=1.0, min=0.1, max=100.0, step=0.1),
12
+ }
13
+
14
+ def setup(self) -> None:
15
+ self.fast_sma = self.add_indicator(
16
+ indicators.SimpleMovingAverage(period=self.fast_period) # type: ignore[attr-defined]
17
+ )
18
+ self.slow_sma = self.add_indicator(
19
+ indicators.SimpleMovingAverage(period=self.slow_period) # type: ignore[attr-defined]
20
+ )
21
+
22
+ def on_bar(self, event: events.BarReceived) -> None:
23
+ if (
24
+ self.fast_sma[-2] <= self.slow_sma[-2]
25
+ and self.fast_sma.latest > self.slow_sma.latest
26
+ and self.position <= 0
27
+ ):
28
+ self.submit_order(
29
+ models.OrderType.MARKET,
30
+ models.OrderSide.BUY,
31
+ self.quantity, # type: ignore[attr-defined]
32
+ action=models.ActionType.ENTRY,
33
+ signal="sma_crossover_up",
34
+ )
35
+
36
+ if (
37
+ self.fast_sma[-2] >= self.slow_sma[-2]
38
+ and self.fast_sma.latest < self.slow_sma.latest
39
+ and self.position >= 0
40
+ ):
41
+ self.submit_order(
42
+ models.OrderType.MARKET,
43
+ models.OrderSide.SELL,
44
+ self.quantity, # type: ignore[attr-defined]
45
+ action=models.ActionType.EXIT,
46
+ signal="sma_crossover_down",
47
+ )
@@ -0,0 +1,3 @@
1
+ __all__ = ["app"]
2
+
3
+ from .app import app