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,12 @@
1
+ from signalflow.strategy.broker.executor.base import OrderExecutor
2
+ from signalflow.core.decorators import sf_component
3
+ from dataclasses import dataclass
4
+
5
+ @dataclass
6
+ @sf_component(name="binance/spot")
7
+
8
+ class BinanceSpotExecutor(OrderExecutor):
9
+ """
10
+ Binance executor for live trading.
11
+ """
12
+ pass
@@ -0,0 +1,81 @@
1
+ """Virtual executor for backtesting - simulates order fills at current prices."""
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import ClassVar
6
+ import uuid
7
+
8
+ from signalflow.core.enums import SfComponentType
9
+ from signalflow.core.decorators import sf_component
10
+
11
+
12
+ @dataclass
13
+ @sf_component(name='virtual/spot', override=True)
14
+ class VirtualSpotExecutor:
15
+ """
16
+ Simulates order execution for backtesting.
17
+
18
+ Fills orders instantly at the provided price with configurable slippage.
19
+ """
20
+ component_type: ClassVar[SfComponentType] = SfComponentType.STRATEGY_EXECUTOR
21
+
22
+ fee_rate: float = 0.001
23
+ slippage_pct: float = 0.0
24
+
25
+ def execute(
26
+ self,
27
+ orders: list,
28
+ prices: dict[str, float],
29
+ ts: datetime
30
+ ) -> list:
31
+ """
32
+ Execute orders at current prices.
33
+
34
+ Args:
35
+ orders: List of Order objects to execute
36
+ prices: Dict mapping pair -> current price
37
+ ts: Current timestamp
38
+
39
+ Returns:
40
+ List of OrderFill objects
41
+ """
42
+ from signalflow.core.containers.order import Order, OrderFill
43
+
44
+ fills: list[OrderFill] = []
45
+
46
+ for order in orders:
47
+ if not isinstance(order, Order):
48
+ continue
49
+
50
+ price = prices.get(order.pair)
51
+ if price is None or price <= 0:
52
+ continue
53
+
54
+ if order.side == 'BUY':
55
+ fill_price = price * (1 + self.slippage_pct)
56
+ else:
57
+ fill_price = price * (1 - self.slippage_pct)
58
+
59
+ notional = fill_price * order.qty
60
+ fee = notional * self.fee_rate
61
+
62
+ fill = OrderFill(
63
+ id=str(uuid.uuid4()),
64
+ order_id=order.id,
65
+ pair=order.pair,
66
+ side=order.side,
67
+ ts=ts,
68
+ price=fill_price,
69
+ qty=order.qty,
70
+ fee=fee,
71
+ position_id=order.position_id,
72
+ meta={
73
+ 'order_meta': order.meta,
74
+ 'signal_strength': order.signal_strength,
75
+ }
76
+ )
77
+ fills.append(fill)
78
+
79
+ order.status = 'FILLED'
80
+
81
+ return fills
@@ -0,0 +1,12 @@
1
+ #TODO: Implement
2
+ from signalflow.strategy.broker.base import Broker
3
+ from signalflow.core.decorators import sf_component
4
+ from dataclasses import dataclass
5
+
6
+ @dataclass
7
+ @sf_component(name="live/spot")
8
+ class RealtimeSpotBroker(Broker):
9
+ """
10
+ Live broker for spot trading.
11
+ """
12
+ pass
@@ -0,0 +1,9 @@
1
+ from signalflow.strategy.component.base import StrategyMetric, ExitRule, EntryRule
2
+ from signalflow.strategy.component.exit import TakeProfitStopLossExit
3
+
4
+ __all__ = [
5
+ "StrategyMetric",
6
+ "ExitRule",
7
+ "EntryRule",
8
+ "TakeProfitStopLossExit",
9
+ ]
@@ -0,0 +1,65 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from typing import ClassVar
4
+ from signalflow.core import SfComponentType, StrategyState, Position, Order, RawData, Signals
5
+
6
+
7
+ @dataclass
8
+ class StrategyMetric(ABC):
9
+ """Base class for strategy metrics."""
10
+ component_type: ClassVar[SfComponentType] = SfComponentType.STRATEGY_METRIC
11
+
12
+ @property
13
+ @abstractmethod
14
+ def name(self) -> str:
15
+ """Metric name for storage."""
16
+ ...
17
+
18
+ @abstractmethod
19
+ def compute(
20
+ self,
21
+ state: StrategyState,
22
+ prices: dict[str, float]
23
+ ) -> dict[str, float]:
24
+ """Compute metric values."""
25
+ ...
26
+
27
+
28
+ @dataclass
29
+ class ExitRule(ABC):
30
+ """Base class for exit rules."""
31
+ component_type: ClassVar[SfComponentType] = SfComponentType.STRATEGY_EXIT_RULE
32
+
33
+ @abstractmethod
34
+ def check_exits(
35
+ self,
36
+ positions: list[Position],
37
+ prices: dict[str, float],
38
+ state: StrategyState
39
+ ) -> list[Order]:
40
+ """
41
+ Check if any positions should be closed.
42
+
43
+ Returns list of close orders.
44
+ """
45
+ ...
46
+
47
+
48
+ @dataclass
49
+ class EntryRule(ABC):
50
+ """Base class for entry rules."""
51
+ component_type: ClassVar[SfComponentType] = SfComponentType.STRATEGY_ENTRY_RULE
52
+
53
+ @abstractmethod
54
+ def check_entries(
55
+ self,
56
+ signals: Signals,
57
+ prices: dict[str, float],
58
+ state: StrategyState
59
+ ) -> list[Order]:
60
+ """
61
+ Check signals and generate entry orders.
62
+
63
+ Returns list of entry orders.
64
+ """
65
+ ...
@@ -0,0 +1,7 @@
1
+ from signalflow.strategy.component.entry.signal import SignalEntryRule
2
+ from signalflow.strategy.component.entry.fixed_size import FixedSizeEntryRule
3
+
4
+ __all__ = [
5
+ "SignalEntryRule",
6
+ "FixedSizeEntryRule",
7
+ ]
@@ -0,0 +1,57 @@
1
+ from dataclasses import dataclass, field
2
+ from signalflow.core import Signals, Order, StrategyState, SignalType, sf_component
3
+ from signalflow.strategy.component.base import EntryRule
4
+ import polars as pl
5
+
6
+ @dataclass
7
+ @sf_component(name='fixed_size_entry')
8
+ class FixedSizeEntryRule(EntryRule):
9
+ """Simple entry rule with fixed position size."""
10
+
11
+ position_size: float = 0.01
12
+ signal_types: list[str] = field(default_factory=lambda: [SignalType.RISE.value])
13
+ max_positions: int = 10
14
+
15
+ pair_col: str = 'pair'
16
+
17
+ def check_entries(
18
+ self,
19
+ signals: Signals,
20
+ prices: dict[str, float],
21
+ state: StrategyState
22
+ ) -> list[Order]:
23
+ orders: list[Order] = []
24
+
25
+ if signals is None or signals.value.height == 0:
26
+ return orders
27
+
28
+ open_count = len(state.portfolio.open_positions())
29
+ if open_count >= self.max_positions:
30
+ return orders
31
+
32
+ df = signals.value.filter(pl.col('signal_type').is_in(self.signal_types))
33
+
34
+ for row in df.iter_rows(named=True):
35
+ if open_count >= self.max_positions:
36
+ break
37
+
38
+ pair = row[self.pair_col]
39
+ signal_type = row['signal_type']
40
+
41
+ price = prices.get(pair)
42
+ if price is None or price <= 0:
43
+ continue
44
+
45
+ side = 'BUY' if signal_type == SignalType.RISE.value else 'SELL'
46
+
47
+ order = Order(
48
+ pair=pair,
49
+ side=side,
50
+ order_type='MARKET',
51
+ qty=self.position_size,
52
+ meta={'signal_type': signal_type}
53
+ )
54
+ orders.append(order)
55
+ open_count += 1
56
+
57
+ return orders
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from signalflow.core import Signals, Order, StrategyState, SignalType, sf_component, Position
5
+ from signalflow.strategy.component.base import EntryRule
6
+ import polars as pl
7
+
8
+
9
+ @dataclass
10
+ @sf_component(name='signal', override=True)
11
+ class SignalEntryRule(EntryRule):
12
+
13
+ base_position_size: float = 100.0
14
+ use_probability_sizing: bool = True
15
+ min_probability: float = 0.5
16
+ max_positions_per_pair: int = 1
17
+ max_total_positions: int = 20
18
+ allow_shorts: bool = False
19
+ max_capital_usage: float = 0.95
20
+ min_order_notional: float = 10.0
21
+ pair_col: str = 'pair'
22
+ ts_col: str = 'timestamp'
23
+
24
+ def check_entries(
25
+ self,
26
+ signals: Signals,
27
+ prices: dict[str, float],
28
+ state: StrategyState
29
+ ) -> list[Order]:
30
+ orders: list[Order] = []
31
+
32
+ if signals is None or signals.value.height == 0:
33
+ return orders
34
+
35
+ positions_by_pair: dict[str, list[Position]] = {}
36
+ for pos in state.portfolio.open_positions():
37
+ positions_by_pair.setdefault(pos.pair, []).append(pos)
38
+
39
+ total_open = len(state.portfolio.open_positions())
40
+ if total_open >= self.max_total_positions:
41
+ return orders
42
+
43
+ available_cash = state.portfolio.cash
44
+
45
+ used_capital = sum(
46
+ pos.entry_price * pos.qty
47
+ for pos in state.portfolio.open_positions()
48
+ )
49
+
50
+ total_equity = available_cash + used_capital
51
+ max_allowed_in_positions = total_equity * self.max_capital_usage
52
+ remaining_allocation = max_allowed_in_positions - used_capital
53
+
54
+ df = signals.value
55
+ actionable_types = [SignalType.RISE.value]
56
+ if self.allow_shorts:
57
+ actionable_types.append(SignalType.FALL.value)
58
+
59
+ df = df.filter(pl.col('signal_type').is_in(actionable_types))
60
+
61
+ if 'probability' in df.columns:
62
+ df = df.filter(pl.col('probability') >= self.min_probability)
63
+ df = df.sort('probability', descending=True)
64
+
65
+ for row in df.iter_rows(named=True):
66
+ if total_open >= self.max_total_positions:
67
+ break
68
+
69
+ if remaining_allocation <= self.min_order_notional:
70
+ break
71
+
72
+ if available_cash <= self.min_order_notional:
73
+ break
74
+
75
+ pair = row[self.pair_col]
76
+ signal_type = row['signal_type']
77
+ probability = row.get('probability', 1.0) or 1.0
78
+
79
+ existing_positions = positions_by_pair.get(pair, [])
80
+ if len(existing_positions) >= self.max_positions_per_pair:
81
+ continue
82
+
83
+ price = prices.get(pair)
84
+ if price is None or price <= 0:
85
+ continue
86
+
87
+ if signal_type == SignalType.RISE.value:
88
+ side = 'BUY'
89
+ elif signal_type == SignalType.FALL.value and self.allow_shorts:
90
+ side = 'SELL'
91
+ else:
92
+ continue
93
+
94
+ notional = self.base_position_size
95
+ if self.use_probability_sizing:
96
+ notional *= probability
97
+
98
+ notional = min(notional, available_cash * 0.99)
99
+ notional = min(notional, remaining_allocation)
100
+
101
+ if notional < self.min_order_notional:
102
+ continue
103
+
104
+ qty = notional / price
105
+
106
+ order = Order(
107
+ pair=pair,
108
+ side=side,
109
+ order_type='MARKET',
110
+ qty=qty,
111
+ signal_strength=probability,
112
+ meta={
113
+ 'signal_type': signal_type,
114
+ 'signal_probability': probability,
115
+ 'signal_ts': row.get(self.ts_col),
116
+ 'requested_notional': notional,
117
+ }
118
+ )
119
+ orders.append(order)
120
+
121
+ total_open += 1
122
+ available_cash -= notional * 1.002
123
+ remaining_allocation -= notional
124
+ positions_by_pair.setdefault(pair, []).append(None)
125
+
126
+ return orders
127
+
@@ -0,0 +1,5 @@
1
+ from signalflow.strategy.component.exit.tp_sl import TakeProfitStopLossExit
2
+
3
+ __all__ = [
4
+ "TakeProfitStopLossExit",
5
+ ]
@@ -0,0 +1,47 @@
1
+ from dataclasses import dataclass
2
+ from signalflow.core import Position, Order, StrategyState, PositionType, sf_component
3
+ from signalflow.strategy.component.base import ExitRule
4
+
5
+ @dataclass
6
+ @sf_component(name='time_exit')
7
+ class TimeBasedExit(ExitRule):
8
+ """Exit positions after a fixed holding period."""
9
+
10
+ max_bars: int = 60
11
+ bar_col: str = 'bar_count'
12
+
13
+ def check_exits(
14
+ self,
15
+ positions: list[Position],
16
+ prices: dict[str, float],
17
+ state: StrategyState
18
+ ) -> list[Order]:
19
+ orders: list[Order] = []
20
+
21
+ for pos in positions:
22
+ if pos.is_closed:
23
+ continue
24
+
25
+ price = prices.get(pos.pair)
26
+ if price is None:
27
+ continue
28
+
29
+ bar_count = pos.meta.get(self.bar_col, 0) + 1
30
+ pos.meta[self.bar_col] = bar_count
31
+
32
+ if bar_count >= self.max_bars:
33
+ side = 'SELL' if pos.position_type == PositionType.LONG else 'BUY'
34
+ order = Order(
35
+ pair=pos.pair,
36
+ side=side,
37
+ order_type='MARKET',
38
+ qty=pos.qty,
39
+ position_id=pos.id,
40
+ meta={
41
+ 'exit_reason': 'time_exit',
42
+ 'bars_held': bar_count,
43
+ }
44
+ )
45
+ orders.append(order)
46
+
47
+ return orders
@@ -0,0 +1,80 @@
1
+ from dataclasses import dataclass
2
+ from signalflow.core import Position, Order, StrategyState, PositionType, sf_component
3
+ from signalflow.strategy.component.base import ExitRule
4
+
5
+
6
+ @dataclass
7
+ @sf_component(name='tp_sl', override=True)
8
+ class TakeProfitStopLossExit(ExitRule):
9
+ """
10
+ Exit rule based on take-profit and stop-loss levels.
11
+
12
+ Can use fixed percentages or dynamic levels from position meta.
13
+ """
14
+
15
+ take_profit_pct: float = 0.02
16
+ stop_loss_pct: float = 0.01
17
+ use_position_levels: bool = False
18
+
19
+ def check_exits(
20
+ self,
21
+ positions: list[Position],
22
+ prices: dict[str, float],
23
+ state: StrategyState
24
+ ) -> list[Order]:
25
+ orders: list[Order] = []
26
+
27
+ for pos in positions:
28
+ if pos.is_closed:
29
+ continue
30
+
31
+ price = prices.get(pos.pair)
32
+ if price is None or price <= 0:
33
+ continue
34
+
35
+ if self.use_position_levels:
36
+ tp_price = pos.meta.get('take_profit_price')
37
+ sl_price = pos.meta.get('stop_loss_price')
38
+ else:
39
+ if pos.position_type == PositionType.LONG:
40
+ tp_price = pos.entry_price * (1 + self.take_profit_pct)
41
+ sl_price = pos.entry_price * (1 - self.stop_loss_pct)
42
+ else:
43
+ tp_price = pos.entry_price * (1 - self.take_profit_pct)
44
+ sl_price = pos.entry_price * (1 + self.stop_loss_pct)
45
+
46
+ should_exit = False
47
+ exit_reason = ''
48
+
49
+ if pos.position_type == PositionType.LONG:
50
+ if tp_price and price >= tp_price:
51
+ should_exit = True
52
+ exit_reason = 'take_profit'
53
+ elif sl_price and price <= sl_price:
54
+ should_exit = True
55
+ exit_reason = 'stop_loss'
56
+ else:
57
+ if tp_price and price <= tp_price:
58
+ should_exit = True
59
+ exit_reason = 'take_profit'
60
+ elif sl_price and price >= sl_price:
61
+ should_exit = True
62
+ exit_reason = 'stop_loss'
63
+
64
+ if should_exit:
65
+ side = 'SELL' if pos.position_type == PositionType.LONG else 'BUY'
66
+ order = Order(
67
+ pair=pos.pair,
68
+ side=side,
69
+ order_type='MARKET',
70
+ qty=pos.qty,
71
+ position_id=pos.id,
72
+ meta={
73
+ 'exit_reason': exit_reason,
74
+ 'entry_price': pos.entry_price,
75
+ 'exit_price': price,
76
+ }
77
+ )
78
+ orders.append(order)
79
+
80
+ return orders
@@ -0,0 +1,8 @@
1
+ from signalflow.strategy.component.metric.main_metrics import TotalReturnMetric, BalanceAllocationMetric, DrawdownMetric, WinRateMetric, SharpeRatioMetric
2
+ __all__ = [
3
+ "TotalReturnMetric",
4
+ "BalanceAllocationMetric",
5
+ "DrawdownMetric",
6
+ "WinRateMetric",
7
+ "SharpeRatioMetric",
8
+ ]