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.
- signalflow/__init__.py +21 -0
- signalflow/analytics/__init__.py +0 -0
- signalflow/core/__init__.py +46 -0
- signalflow/core/base_mixin.py +232 -0
- signalflow/core/containers/__init__.py +21 -0
- signalflow/core/containers/order.py +216 -0
- signalflow/core/containers/portfolio.py +211 -0
- signalflow/core/containers/position.py +296 -0
- signalflow/core/containers/raw_data.py +167 -0
- signalflow/core/containers/raw_data_view.py +169 -0
- signalflow/core/containers/signals.py +198 -0
- signalflow/core/containers/strategy_state.py +147 -0
- signalflow/core/containers/trade.py +112 -0
- signalflow/core/decorators.py +103 -0
- signalflow/core/enums.py +270 -0
- signalflow/core/registry.py +322 -0
- signalflow/core/rolling_aggregator.py +362 -0
- signalflow/core/signal_transforms/__init__.py +5 -0
- signalflow/core/signal_transforms/base_signal_transform.py +186 -0
- signalflow/data/__init__.py +11 -0
- signalflow/data/raw_data_factory.py +225 -0
- signalflow/data/raw_store/__init__.py +7 -0
- signalflow/data/raw_store/base.py +271 -0
- signalflow/data/raw_store/duckdb_stores.py +696 -0
- signalflow/data/source/__init__.py +10 -0
- signalflow/data/source/base.py +300 -0
- signalflow/data/source/binance.py +442 -0
- signalflow/data/strategy_store/__init__.py +8 -0
- signalflow/data/strategy_store/base.py +278 -0
- signalflow/data/strategy_store/duckdb.py +409 -0
- signalflow/data/strategy_store/schema.py +36 -0
- signalflow/detector/__init__.py +7 -0
- signalflow/detector/adapter/__init__.py +5 -0
- signalflow/detector/adapter/pandas_detector.py +46 -0
- signalflow/detector/base.py +390 -0
- signalflow/detector/sma_cross.py +105 -0
- signalflow/feature/__init__.py +16 -0
- signalflow/feature/adapter/__init__.py +5 -0
- signalflow/feature/adapter/pandas_feature_extractor.py +54 -0
- signalflow/feature/base.py +330 -0
- signalflow/feature/feature_set.py +286 -0
- signalflow/feature/oscillator/__init__.py +5 -0
- signalflow/feature/oscillator/rsi_extractor.py +42 -0
- signalflow/feature/pandasta/__init__.py +10 -0
- signalflow/feature/pandasta/pandas_ta_extractor.py +141 -0
- signalflow/feature/pandasta/top_pandasta_extractors.py +64 -0
- signalflow/feature/smoother/__init__.py +5 -0
- signalflow/feature/smoother/sma_extractor.py +46 -0
- signalflow/strategy/__init__.py +9 -0
- signalflow/strategy/broker/__init__.py +15 -0
- signalflow/strategy/broker/backtest.py +172 -0
- signalflow/strategy/broker/base.py +186 -0
- signalflow/strategy/broker/executor/__init__.py +9 -0
- signalflow/strategy/broker/executor/base.py +35 -0
- signalflow/strategy/broker/executor/binance_spot.py +12 -0
- signalflow/strategy/broker/executor/virtual_spot.py +81 -0
- signalflow/strategy/broker/realtime_spot.py +12 -0
- signalflow/strategy/component/__init__.py +9 -0
- signalflow/strategy/component/base.py +65 -0
- signalflow/strategy/component/entry/__init__.py +7 -0
- signalflow/strategy/component/entry/fixed_size.py +57 -0
- signalflow/strategy/component/entry/signal.py +127 -0
- signalflow/strategy/component/exit/__init__.py +5 -0
- signalflow/strategy/component/exit/time_based.py +47 -0
- signalflow/strategy/component/exit/tp_sl.py +80 -0
- signalflow/strategy/component/metric/__init__.py +8 -0
- signalflow/strategy/component/metric/main_metrics.py +181 -0
- signalflow/strategy/runner/__init__.py +8 -0
- signalflow/strategy/runner/backtest_runner.py +208 -0
- signalflow/strategy/runner/base.py +19 -0
- signalflow/strategy/runner/optimized_backtest_runner.py +178 -0
- signalflow/strategy/runner/realtime_runner.py +0 -0
- signalflow/target/__init__.py +14 -0
- signalflow/target/adapter/__init__.py +5 -0
- signalflow/target/adapter/pandas_labeler.py +45 -0
- signalflow/target/base.py +409 -0
- signalflow/target/fixed_horizon_labeler.py +93 -0
- signalflow/target/static_triple_barrier.py +162 -0
- signalflow/target/triple_barrier.py +188 -0
- signalflow/utils/__init__.py +7 -0
- signalflow/utils/import_utils.py +11 -0
- signalflow/utils/tune_utils.py +19 -0
- signalflow/validator/__init__.py +6 -0
- signalflow/validator/base.py +139 -0
- signalflow/validator/sklearn_validator.py +527 -0
- signalflow_trading-0.2.1.dist-info/METADATA +149 -0
- signalflow_trading-0.2.1.dist-info/RECORD +90 -0
- signalflow_trading-0.2.1.dist-info/WHEEL +5 -0
- signalflow_trading-0.2.1.dist-info/licenses/LICENSE +21 -0
- 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,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,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,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
|
+
]
|