signalflow-trading 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. signalflow/__init__.py +21 -0
  2. signalflow/analytics/__init__.py +0 -0
  3. signalflow/core/__init__.py +46 -0
  4. signalflow/core/base_mixin.py +232 -0
  5. signalflow/core/containers/__init__.py +21 -0
  6. signalflow/core/containers/order.py +216 -0
  7. signalflow/core/containers/portfolio.py +211 -0
  8. signalflow/core/containers/position.py +296 -0
  9. signalflow/core/containers/raw_data.py +167 -0
  10. signalflow/core/containers/raw_data_view.py +169 -0
  11. signalflow/core/containers/signals.py +198 -0
  12. signalflow/core/containers/strategy_state.py +147 -0
  13. signalflow/core/containers/trade.py +112 -0
  14. signalflow/core/decorators.py +103 -0
  15. signalflow/core/enums.py +270 -0
  16. signalflow/core/registry.py +322 -0
  17. signalflow/core/rolling_aggregator.py +362 -0
  18. signalflow/core/signal_transforms/__init__.py +5 -0
  19. signalflow/core/signal_transforms/base_signal_transform.py +186 -0
  20. signalflow/data/__init__.py +11 -0
  21. signalflow/data/raw_data_factory.py +225 -0
  22. signalflow/data/raw_store/__init__.py +7 -0
  23. signalflow/data/raw_store/base.py +271 -0
  24. signalflow/data/raw_store/duckdb_stores.py +696 -0
  25. signalflow/data/source/__init__.py +10 -0
  26. signalflow/data/source/base.py +300 -0
  27. signalflow/data/source/binance.py +442 -0
  28. signalflow/data/strategy_store/__init__.py +8 -0
  29. signalflow/data/strategy_store/base.py +278 -0
  30. signalflow/data/strategy_store/duckdb.py +409 -0
  31. signalflow/data/strategy_store/schema.py +36 -0
  32. signalflow/detector/__init__.py +7 -0
  33. signalflow/detector/adapter/__init__.py +5 -0
  34. signalflow/detector/adapter/pandas_detector.py +46 -0
  35. signalflow/detector/base.py +390 -0
  36. signalflow/detector/sma_cross.py +105 -0
  37. signalflow/feature/__init__.py +16 -0
  38. signalflow/feature/adapter/__init__.py +5 -0
  39. signalflow/feature/adapter/pandas_feature_extractor.py +54 -0
  40. signalflow/feature/base.py +330 -0
  41. signalflow/feature/feature_set.py +286 -0
  42. signalflow/feature/oscillator/__init__.py +5 -0
  43. signalflow/feature/oscillator/rsi_extractor.py +42 -0
  44. signalflow/feature/pandasta/__init__.py +10 -0
  45. signalflow/feature/pandasta/pandas_ta_extractor.py +141 -0
  46. signalflow/feature/pandasta/top_pandasta_extractors.py +64 -0
  47. signalflow/feature/smoother/__init__.py +5 -0
  48. signalflow/feature/smoother/sma_extractor.py +46 -0
  49. signalflow/strategy/__init__.py +9 -0
  50. signalflow/strategy/broker/__init__.py +15 -0
  51. signalflow/strategy/broker/backtest.py +172 -0
  52. signalflow/strategy/broker/base.py +186 -0
  53. signalflow/strategy/broker/executor/__init__.py +9 -0
  54. signalflow/strategy/broker/executor/base.py +35 -0
  55. signalflow/strategy/broker/executor/binance_spot.py +12 -0
  56. signalflow/strategy/broker/executor/virtual_spot.py +81 -0
  57. signalflow/strategy/broker/realtime_spot.py +12 -0
  58. signalflow/strategy/component/__init__.py +9 -0
  59. signalflow/strategy/component/base.py +65 -0
  60. signalflow/strategy/component/entry/__init__.py +7 -0
  61. signalflow/strategy/component/entry/fixed_size.py +57 -0
  62. signalflow/strategy/component/entry/signal.py +127 -0
  63. signalflow/strategy/component/exit/__init__.py +5 -0
  64. signalflow/strategy/component/exit/time_based.py +47 -0
  65. signalflow/strategy/component/exit/tp_sl.py +80 -0
  66. signalflow/strategy/component/metric/__init__.py +8 -0
  67. signalflow/strategy/component/metric/main_metrics.py +181 -0
  68. signalflow/strategy/runner/__init__.py +8 -0
  69. signalflow/strategy/runner/backtest_runner.py +208 -0
  70. signalflow/strategy/runner/base.py +19 -0
  71. signalflow/strategy/runner/optimized_backtest_runner.py +178 -0
  72. signalflow/strategy/runner/realtime_runner.py +0 -0
  73. signalflow/target/__init__.py +14 -0
  74. signalflow/target/adapter/__init__.py +5 -0
  75. signalflow/target/adapter/pandas_labeler.py +45 -0
  76. signalflow/target/base.py +409 -0
  77. signalflow/target/fixed_horizon_labeler.py +93 -0
  78. signalflow/target/static_triple_barrier.py +162 -0
  79. signalflow/target/triple_barrier.py +188 -0
  80. signalflow/utils/__init__.py +7 -0
  81. signalflow/utils/import_utils.py +11 -0
  82. signalflow/utils/tune_utils.py +19 -0
  83. signalflow/validator/__init__.py +6 -0
  84. signalflow/validator/base.py +139 -0
  85. signalflow/validator/sklearn_validator.py +527 -0
  86. signalflow_trading-0.2.1.dist-info/METADATA +149 -0
  87. signalflow_trading-0.2.1.dist-info/RECORD +90 -0
  88. signalflow_trading-0.2.1.dist-info/WHEEL +5 -0
  89. signalflow_trading-0.2.1.dist-info/licenses/LICENSE +21 -0
  90. signalflow_trading-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from typing import Any, Iterable, Literal
7
+
8
+ import polars as pl
9
+
10
+ from signalflow.core.containers.position import Position
11
+ from signalflow.core.containers.trade import Trade
12
+
13
+ @dataclass(slots=True)
14
+ class Portfolio:
15
+ """Portfolio snapshot (pure domain).
16
+
17
+ Tracks cash and open/closed positions. Provides equity calculation
18
+ and DataFrame conversion utilities.
19
+
20
+ Portfolio state should only be modified through broker operations
21
+ to maintain accounting consistency.
22
+
23
+ Attributes:
24
+ cash (float): Available cash balance.
25
+ positions (dict[str, Position]): Dictionary of positions keyed by position ID.
26
+
27
+ Example:
28
+ ```python
29
+ from signalflow.core import Portfolio, Position, PositionType
30
+
31
+ # Initialize portfolio
32
+ portfolio = Portfolio(cash=10000.0)
33
+
34
+ # Add position
35
+ position = Position(
36
+ pair="BTCUSDT",
37
+ position_type=PositionType.LONG,
38
+ entry_price=45000.0,
39
+ qty=0.5
40
+ )
41
+ portfolio.positions[position.id] = position
42
+
43
+ # Calculate equity
44
+ prices = {"BTCUSDT": 46000.0, "ETHUSDT": 3200.0}
45
+ equity = portfolio.equity(prices=prices)
46
+
47
+ # Get open positions
48
+ open_positions = portfolio.open_positions()
49
+
50
+ # Convert to DataFrame
51
+ positions_df = Portfolio.positions_to_pl(open_positions)
52
+ ```
53
+
54
+ Note:
55
+ Portfolio state should only be modified through broker operations.
56
+ Direct manipulation may lead to accounting inconsistencies.
57
+ """
58
+
59
+ cash: float = 0.0
60
+ positions: dict[str, Position] = field(default_factory=dict)
61
+
62
+ def open_positions(self) -> list[Position]:
63
+ """Get list of open (non-closed) positions.
64
+
65
+ Returns:
66
+ list[Position]: Open positions.
67
+
68
+ Example:
69
+ ```python
70
+ open = portfolio.open_positions()
71
+ print(f"Open positions: {len(open)}")
72
+
73
+ for pos in open:
74
+ print(f"{pos.pair}: {pos.qty} @ ${pos.entry_price}")
75
+ ```
76
+ """
77
+ return [p for p in self.positions.values() if not p.is_closed]
78
+
79
+ def equity(self, *, prices: dict[str, float]) -> float:
80
+ """Calculate total portfolio equity.
81
+
82
+ Equity = cash + sum(side_sign * price * qty for all positions)
83
+
84
+ Executor must keep accounting consistent for accurate equity.
85
+
86
+ Args:
87
+ prices (dict[str, float]): Current prices per pair.
88
+
89
+ Returns:
90
+ float: Total equity in currency units.
91
+
92
+ Example:
93
+ ```python
94
+ prices = {
95
+ "BTCUSDT": 46000.0,
96
+ "ETHUSDT": 3200.0
97
+ }
98
+ equity = portfolio.equity(prices=prices)
99
+ print(f"Total equity: ${equity:,.2f}")
100
+
101
+ # Track equity over time
102
+ equity_history = []
103
+ for ts, prices in price_history:
104
+ eq = portfolio.equity(prices=prices)
105
+ equity_history.append((ts, eq))
106
+ ```
107
+
108
+ Note:
109
+ If price not in dict, uses position's last_price.
110
+ Executor must keep accounting consistent.
111
+ """
112
+ eq = self.cash
113
+ for p in self.positions.values():
114
+ px = prices.get(p.pair, p.last_price)
115
+ eq += p.side_sign * px * p.qty
116
+ return eq
117
+
118
+ @staticmethod
119
+ def positions_to_pl(positions: Iterable[Position]) -> pl.DataFrame:
120
+ """Convert positions to Polars DataFrame.
121
+
122
+ Args:
123
+ positions (Iterable[Position]): Positions to convert.
124
+
125
+ Returns:
126
+ pl.DataFrame: DataFrame with position data. Empty if no positions.
127
+
128
+ Example:
129
+ ```python
130
+ # Convert all positions
131
+ all_df = Portfolio.positions_to_pl(portfolio.positions.values())
132
+
133
+ # Convert only open positions
134
+ open_df = Portfolio.positions_to_pl(portfolio.open_positions())
135
+
136
+ # Analyze positions
137
+ print(open_df.select(["pair", "qty", "realized_pnl"]))
138
+
139
+ # Group by pair
140
+ by_pair = open_df.group_by("pair").agg([
141
+ pl.col("qty").sum().alias("total_qty"),
142
+ pl.col("realized_pnl").sum().alias("total_pnl")
143
+ ])
144
+ ```
145
+ """
146
+ if not positions:
147
+ return pl.DataFrame()
148
+ return pl.DataFrame([
149
+ {
150
+ "id": p.id,
151
+ "is_closed": p.is_closed,
152
+ "pair": p.pair,
153
+ "position_type": p.position_type.value,
154
+ "signal_strength": p.signal_strength,
155
+ "entry_time": p.entry_time,
156
+ "last_time": p.last_time,
157
+ "entry_price": p.entry_price,
158
+ "last_price": p.last_price,
159
+ "qty": p.qty,
160
+ "fees_paid": p.fees_paid,
161
+ "realized_pnl": p.realized_pnl,
162
+ "meta": p.meta,
163
+ }
164
+ for p in positions
165
+ ])
166
+
167
+ @staticmethod
168
+ def trades_to_pl(trades: Iterable[Trade]) -> pl.DataFrame:
169
+ """Convert trades to Polars DataFrame.
170
+
171
+ Args:
172
+ trades (Iterable[Trade]): Trades to convert.
173
+
174
+ Returns:
175
+ pl.DataFrame: DataFrame with trade data. Empty if no trades.
176
+
177
+ Example:
178
+ ```python
179
+ # Convert all trades
180
+ trades_df = Portfolio.trades_to_pl(all_trades)
181
+
182
+ # Analyze trades
183
+ print(trades_df.select(["pair", "side", "price", "qty"]))
184
+
185
+ # Filter by type
186
+ entry_trades = trades_df.filter(
187
+ pl.col("meta").struct.field("type") == "entry"
188
+ )
189
+
190
+ # Calculate total volume
191
+ total_volume = trades_df.select(
192
+ (pl.col("price") * pl.col("qty")).sum()
193
+ ).item()
194
+ ```
195
+ """
196
+ if not trades:
197
+ return pl.DataFrame()
198
+ return pl.DataFrame([
199
+ {
200
+ "id": t.id,
201
+ "position_id": t.position_id,
202
+ "pair": t.pair,
203
+ "side": t.side,
204
+ "ts": t.ts,
205
+ "price": t.price,
206
+ "qty": t.qty,
207
+ "fee": t.fee,
208
+ "meta": t.meta,
209
+ }
210
+ for t in trades
211
+ ])
@@ -0,0 +1,296 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from typing import Any, Iterable, Literal
7
+
8
+ import polars as pl
9
+
10
+ from signalflow.core.containers.trade import Trade
11
+ from signalflow.core.containers.trade import TradeSide
12
+ from signalflow.core.enums import PositionType
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class Position:
17
+ """Trading position aggregate.
18
+
19
+ Mutable by design - tracks the lifecycle of a trading position through
20
+ multiple trades (entry, partial exits, full exit).
21
+
22
+ Position state changes through two operations:
23
+ - mark(): Update to current market price (mark-to-market)
24
+ - apply_trade(): Apply executed trade to position
25
+
26
+ Attributes:
27
+ id (str): Unique position identifier.
28
+ is_closed (bool): Whether position is closed.
29
+ pair (str): Trading pair (e.g. "BTCUSDT").
30
+ position_type (PositionType): LONG or SHORT.
31
+ signal_strength (float): Strength of initial signal (0-1).
32
+ entry_time (datetime | None): Position entry timestamp.
33
+ last_time (datetime | None): Last update timestamp.
34
+ entry_price (float): Average entry price.
35
+ last_price (float): Current/last marked price.
36
+ qty (float): Current quantity held.
37
+ fees_paid (float): Total fees paid.
38
+ realized_pnl (float): Realized profit/loss.
39
+ meta (dict[str, Any]): Additional metadata.
40
+
41
+ Example:
42
+ ```python
43
+ from signalflow.core import Position, PositionType, Trade
44
+ from datetime import datetime
45
+
46
+ # Create position
47
+ position = Position(
48
+ pair="BTCUSDT",
49
+ position_type=PositionType.LONG,
50
+ entry_price=45000.0,
51
+ qty=0.5,
52
+ signal_strength=0.85,
53
+ entry_time=datetime.now()
54
+ )
55
+
56
+ # Mark to market
57
+ position.mark(ts=datetime.now(), price=46000.0)
58
+
59
+ # Apply exit trade
60
+ exit_trade = Trade(
61
+ pair="BTCUSDT",
62
+ side="SELL",
63
+ ts=datetime.now(),
64
+ price=46500.0,
65
+ qty=0.5,
66
+ fee=23.25
67
+ )
68
+ position.apply_trade(exit_trade)
69
+
70
+ # Check results
71
+ print(f"Total PnL: ${position.total_pnl:.2f}")
72
+ print(f"Closed: {position.is_closed}")
73
+ ```
74
+
75
+ Note:
76
+ Position changes ONLY through apply_trade() and mark().
77
+ Direct attribute modification should be avoided.
78
+ """
79
+
80
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
81
+ is_closed: bool = False
82
+
83
+ pair: str = ""
84
+ position_type: PositionType = PositionType.LONG
85
+ signal_strength: float = 1.0
86
+
87
+ entry_time: datetime | None = None
88
+ last_time: datetime | None = None
89
+
90
+ entry_price: float = 0.0
91
+ last_price: float = 0.0
92
+
93
+ qty: float = 0.0
94
+ fees_paid: float = 0.0
95
+ realized_pnl: float = 0.0
96
+
97
+ meta: dict[str, Any] = field(default_factory=dict)
98
+
99
+ @property
100
+ def side_sign(self) -> float:
101
+ """Position direction multiplier.
102
+
103
+ Returns:
104
+ float: 1.0 for LONG, -1.0 for SHORT.
105
+
106
+ Example:
107
+ ```python
108
+ long_pos = Position(position_type=PositionType.LONG)
109
+ assert long_pos.side_sign == 1.0
110
+
111
+ short_pos = Position(position_type=PositionType.SHORT)
112
+ assert short_pos.side_sign == -1.0
113
+ ```
114
+ """
115
+ return 1.0 if self.position_type == PositionType.LONG else -1.0
116
+
117
+ @property
118
+ def unrealized_pnl(self) -> float:
119
+ """Unrealized profit/loss at current price.
120
+
121
+ Calculated as: side_sign * (last_price - entry_price) * qty
122
+
123
+ Returns:
124
+ float: Unrealized PnL in currency units.
125
+
126
+ Example:
127
+ ```python
128
+ position = Position(
129
+ position_type=PositionType.LONG,
130
+ entry_price=45000.0,
131
+ last_price=46000.0,
132
+ qty=0.5
133
+ )
134
+ # (46000 - 45000) * 0.5 = 500
135
+ assert position.unrealized_pnl == 500.0
136
+ ```
137
+ """
138
+ return self.side_sign * (self.last_price - self.entry_price) * self.qty
139
+
140
+ @property
141
+ def total_pnl(self) -> float:
142
+ """Total profit/loss including fees.
143
+
144
+ Calculated as: realized_pnl + unrealized_pnl - fees_paid
145
+
146
+ Returns:
147
+ float: Total PnL in currency units.
148
+
149
+ Example:
150
+ ```python
151
+ position = Position(
152
+ realized_pnl=100.0,
153
+ fees_paid=50.0
154
+ )
155
+ position.mark(ts=datetime.now(), price=46000.0)
156
+ total = position.total_pnl # realized + unrealized - fees
157
+ ```
158
+ """
159
+ return self.realized_pnl + self.unrealized_pnl - self.fees_paid
160
+
161
+ def mark(self, *, ts: datetime, price: float) -> None:
162
+ """Update position to current market price (mark-to-market).
163
+
164
+ Updates last_time and last_price without affecting position size.
165
+ Used for tracking unrealized PnL over time.
166
+
167
+ Args:
168
+ ts (datetime): Update timestamp.
169
+ price (float): Current market price.
170
+
171
+ Example:
172
+ ```python
173
+ # Mark position at each bar
174
+ for bar in bars:
175
+ position.mark(ts=bar.timestamp, price=bar.close)
176
+ print(f"Unrealized PnL: ${position.unrealized_pnl:.2f}")
177
+ ```
178
+ """
179
+ self.last_time = ts
180
+ self.last_price = float(price)
181
+
182
+ def apply_trade(self, trade: Trade) -> None:
183
+ """Apply trade fill to position.
184
+
185
+ Updates position state based on trade:
186
+ - Increases position if trade direction matches position type
187
+ - Decreases position (partial/full close) if direction opposes
188
+ - Updates fees, realized PnL, and closing status
189
+
190
+ Trade processing:
191
+ - BUY increases LONG, SELL decreases LONG
192
+ - SELL increases SHORT, BUY decreases SHORT
193
+ - Entry price updated as weighted average on increases
194
+ - Realized PnL computed on decreases
195
+
196
+ Args:
197
+ trade (Trade): Trade to apply. Must have qty > 0.
198
+
199
+ Example:
200
+ ```python
201
+ # Open position
202
+ entry_trade = Trade(
203
+ pair="BTCUSDT",
204
+ side="BUY",
205
+ price=45000.0,
206
+ qty=1.0,
207
+ fee=45.0
208
+ )
209
+ position.apply_trade(entry_trade)
210
+
211
+ # Partial close
212
+ exit_trade = Trade(
213
+ pair="BTCUSDT",
214
+ side="SELL",
215
+ price=46000.0,
216
+ qty=0.5,
217
+ fee=23.0
218
+ )
219
+ position.apply_trade(exit_trade)
220
+
221
+ # Check state
222
+ assert position.qty == 0.5
223
+ assert position.realized_pnl == 500.0 # (46000-45000)*0.5
224
+ assert not position.is_closed
225
+ ```
226
+
227
+ Note:
228
+ Assumes trade.qty > 0. Position automatically marked as closed
229
+ when qty reaches 0.
230
+ """
231
+ self.last_time = trade.ts
232
+ self.last_price = float(trade.price)
233
+ self.fees_paid += float(trade.fee)
234
+
235
+ is_increase = self._is_increase(trade.side)
236
+
237
+ if is_increase:
238
+ self._increase(trade)
239
+ else:
240
+ self._decrease(trade)
241
+
242
+ def _is_increase(self, side: TradeSide) -> bool:
243
+ """Check if trade increases position size.
244
+
245
+ Args:
246
+ side (TradeSide): Trade side ("BUY" or "SELL").
247
+
248
+ Returns:
249
+ bool: True if trade increases position.
250
+ """
251
+ return (
252
+ (self.position_type == PositionType.LONG and side == "BUY")
253
+ or (self.position_type == PositionType.SHORT and side == "SELL")
254
+ )
255
+
256
+ def _increase(self, trade: Trade) -> None:
257
+ """Increase position size with new trade.
258
+
259
+ Updates entry_price as weighted average of existing and new position.
260
+
261
+ Args:
262
+ trade (Trade): Trade to add to position.
263
+ """
264
+ new_qty = self.qty + trade.qty
265
+ if new_qty <= 0:
266
+ return
267
+
268
+ if self.qty == 0:
269
+ self.entry_price = trade.price
270
+ self.entry_time = trade.ts
271
+ else:
272
+ self.entry_price = (
273
+ self.entry_price * self.qty + trade.price * trade.qty
274
+ ) / new_qty
275
+
276
+ self.qty = new_qty
277
+
278
+ def _decrease(self, trade: Trade) -> None:
279
+ """Decrease position size (partial or full close).
280
+
281
+ Computes realized PnL for closed portion.
282
+ Marks position as closed if qty reaches 0.
283
+
284
+ Args:
285
+ trade (Trade): Trade to close position.
286
+ """
287
+ close_qty = min(self.qty, trade.qty)
288
+ if close_qty <= 0:
289
+ return
290
+
291
+ pnl = self.side_sign * (trade.price - self.entry_price) * close_qty
292
+ self.realized_pnl += pnl
293
+ self.qty -= close_qty
294
+
295
+ if self.qty == 0:
296
+ self.is_closed = True
@@ -0,0 +1,167 @@
1
+ import pandas as pd
2
+ import polars as pl
3
+
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from typing import Iterator
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class RawData:
11
+ """Immutable container for raw market data.
12
+
13
+ Acts as a unified in-memory bundle for multiple raw datasets
14
+ (e.g. spot prices, funding, trades, orderbook, signals).
15
+
16
+ Design principles:
17
+ - Canonical storage is dataset-based (dictionary by name)
18
+ - Datasets accessed via string keys (e.g. raw_data["spot"])
19
+ - No business logic or transformations
20
+ - Immutability ensures reproducibility in pipelines
21
+
22
+ Attributes:
23
+ datetime_start (datetime): Start datetime of the data snapshot.
24
+ datetime_end (datetime): End datetime of the data snapshot.
25
+ pairs (list[str]): List of trading pairs in the snapshot.
26
+ data (dict[str, pl.DataFrame]): Dictionary of datasets keyed by name.
27
+
28
+ Example:
29
+ ```python
30
+ from signalflow.core import RawData
31
+ import polars as pl
32
+ from datetime import datetime
33
+
34
+ # Create RawData with spot data
35
+ raw_data = RawData(
36
+ datetime_start=datetime(2024, 1, 1),
37
+ datetime_end=datetime(2024, 12, 31),
38
+ pairs=["BTCUSDT", "ETHUSDT"],
39
+ data={
40
+ "spot": spot_dataframe,
41
+ "signals": signals_dataframe,
42
+ }
43
+ )
44
+
45
+ # Access datasets
46
+ spot_df = raw_data["spot"]
47
+ signals_df = raw_data.get("signals")
48
+
49
+ # Check if dataset exists
50
+ if "spot" in raw_data:
51
+ print("Spot data available")
52
+ ```
53
+
54
+ Note:
55
+ Dataset schemas are defined by convention, not enforced.
56
+ Views (pandas/polars) should be handled by RawDataView wrapper.
57
+ """
58
+
59
+ datetime_start: datetime
60
+ datetime_end: datetime
61
+ pairs: list[str] = field(default_factory=list)
62
+ data: dict[str, pl.DataFrame] = field(default_factory=dict)
63
+
64
+ def get(self, key: str) -> pl.DataFrame:
65
+ """Get dataset by key.
66
+
67
+ Args:
68
+ key (str): Dataset name (e.g. "spot", "signals").
69
+
70
+ Returns:
71
+ pl.DataFrame: Polars DataFrame if exists, empty DataFrame otherwise.
72
+
73
+ Raises:
74
+ TypeError: If dataset exists but is not a Polars DataFrame.
75
+
76
+ Example:
77
+ ```python
78
+ spot_df = raw_data.get("spot")
79
+
80
+ # Returns empty DataFrame if key doesn't exist
81
+ missing_df = raw_data.get("nonexistent")
82
+ assert missing_df.is_empty()
83
+ ```
84
+ """
85
+ obj = self.data.get(key)
86
+ if obj is None:
87
+ return pl.DataFrame()
88
+ if not isinstance(obj, pl.DataFrame):
89
+ raise TypeError(
90
+ f"Dataset '{key}' is not a polars.DataFrame: {type(obj)}"
91
+ )
92
+ return obj
93
+
94
+ def __getitem__(self, key: str) -> pl.DataFrame:
95
+ """Dictionary-style access to datasets.
96
+
97
+ Args:
98
+ key (str): Dataset name.
99
+
100
+ Returns:
101
+ pl.DataFrame: Dataset as Polars DataFrame.
102
+
103
+ Example:
104
+ ```python
105
+ spot_df = raw_data["spot"]
106
+ ```
107
+ """
108
+ return self.get(key)
109
+
110
+ def __contains__(self, key: str) -> bool:
111
+ """Check if dataset exists.
112
+
113
+ Args:
114
+ key (str): Dataset name to check.
115
+
116
+ Returns:
117
+ bool: True if dataset exists, False otherwise.
118
+
119
+ Example:
120
+ ```python
121
+ if "spot" in raw_data:
122
+ process_spot_data(raw_data["spot"])
123
+ ```
124
+ """
125
+ return key in self.data
126
+
127
+ def keys(self) -> Iterator[str]:
128
+ """Return available dataset keys.
129
+
130
+ Returns:
131
+ Iterator[str]: Iterator over dataset names.
132
+
133
+ Example:
134
+ ```python
135
+ for key in raw_data.keys():
136
+ print(f"Dataset: {key}")
137
+ ```
138
+ """
139
+ return self.data.keys()
140
+
141
+ def items(self):
142
+ """Return (key, dataset) pairs.
143
+
144
+ Returns:
145
+ Iterator: Iterator over (key, DataFrame) tuples.
146
+
147
+ Example:
148
+ ```python
149
+ for name, df in raw_data.items():
150
+ print(f"{name}: {df.shape}")
151
+ ```
152
+ """
153
+ return self.data.items()
154
+
155
+ def values(self):
156
+ """Return dataset values.
157
+
158
+ Returns:
159
+ Iterator: Iterator over DataFrames.
160
+
161
+ Example:
162
+ ```python
163
+ for df in raw_data.values():
164
+ print(df.columns)
165
+ ```
166
+ """
167
+ return self.data.values()