Skip to content

Commit 5c1c183

Browse files
committed
[Strategy] Add Dual Thrust Strategy
1 parent 35e16ea commit 5c1c183

3 files changed

Lines changed: 89 additions & 1 deletion

File tree

src/quanteval/strategies/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from quanteval.strategies.dual_ma import DualMAStrategy
44
from quanteval.strategies.bollinger_mean_reversion import BollingerMeanReversionStrategy
55
from quanteval.strategies.buy_hold import BuyAndHoldStrategy
6+
from quanteval.strategies.dual_thrust import DualThrustStrategy
67

78
__all__ = [
89
'DualMAStrategy',
910
'BollingerMeanReversionStrategy',
1011
'BuyAndHoldStrategy',
12+
'DualThrustStrategy',
1113
]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Dual Thrust Strategy -
3+
Strategy Logic:
4+
- Buy: When price breaks above the upper threshold (based on previous range)
5+
- Sell: When price breaks below the lower threshold (based on previous range)
6+
策略逻辑:
7+
- 买入:当价格突破上轨(基于前一天的区间)
8+
- 卖出:当价格突破下轨(基于前一天的区间)
9+
"""
10+
11+
import pandas as pd
12+
import numpy as np
13+
from quanteval.core.strategy import Strategy
14+
15+
16+
class DualThrustStrategy(Strategy):
17+
"""
18+
Dual Thrust Strategy - A breakout strategy based on price range.
19+
20+
双重突破策略,基于价格区间的突破策略。
21+
22+
Strategy Logic:
23+
- Buy: When price breaks above the upper threshold (based on previous range)
24+
- Sell: When price breaks below the lower threshold (based on previous range)
25+
策略逻辑:
26+
- 买入:当价格突破上轨(基于前一天的区间)
27+
- 卖出:当价格突破下轨(基于前一天的区间)
28+
Args:
29+
k1: 上轨系数 (Upper threshold multiplier, default 0.5)
30+
k2: 下轨系数 (Lower threshold multiplier, default 0.5)
31+
32+
Example:
33+
>>> strategy = DualThrustStrategy(k1=0.5, k2=0.5)
34+
>>> bt = Backtester(strategy, data)
35+
>>> results = bt.run()
36+
"""
37+
38+
def __init__(self, k1: float = 0.5, k2: float = 0.5, window: int = 5):
39+
super().__init__(
40+
name=f'DualThrust(k1={k1},k2={k2}, N={window})', k1=k1, k2=k2, window=window
41+
)
42+
43+
def generate_signals(self, data: pd.DataFrame) -> pd.Series:
44+
"""
45+
生成交易信号
46+
47+
Generate trading signals based on Dual Thrust logic.
48+
49+
Returns:
50+
Series with values:
51+
1: Long position (price > upper threshold)
52+
-1: Short position (price < lower threshold)
53+
0: No position
54+
"""
55+
k1 = self.params['k1']
56+
k2 = self.params['k2']
57+
n = self.params['window']
58+
59+
# Calculate previous day's range
60+
hh = data['High'].shift(1).rolling(window=n).max()
61+
hc = data['Close'].shift(1).rolling(window=n).max()
62+
ll = data['Low'].shift(1).rolling(window=n).min()
63+
lc = data['Close'].shift(1).rolling(window=n).min()
64+
diff = np.maximum(hh - lc, hc - ll)
65+
66+
# Calculate thresholds
67+
upper_threshold = data['Open'] + k1 * diff
68+
lower_threshold = data['Open'] - k2 * diff
69+
70+
# Generate signals
71+
signals = pd.Series(0, index=data.index)
72+
signals[data['High'] > upper_threshold] = 1 # Buy signal
73+
signals[data['Low'] < lower_threshold] = 0 # Sell signal
74+
75+
return signals

tests/test_strategies.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import pandas as pd
22

3-
from quanteval import BollingerMeanReversionStrategy, BuyAndHoldStrategy, DualMAStrategy
3+
from quanteval.strategies import (
4+
BollingerMeanReversionStrategy,
5+
BuyAndHoldStrategy,
6+
DualMAStrategy,
7+
DualThrustStrategy,
8+
)
49

510

611
def test_buy_and_hold_always_long(sample_market_data: pd.DataFrame) -> None:
@@ -20,3 +25,9 @@ def test_bollinger_strategy_returns_position_series(sample_market_data: pd.DataF
2025
)
2126
assert signal.index.equals(sample_market_data.index)
2227
assert set(signal.unique()).issubset({0.0, 1.0})
28+
29+
30+
def test_dual_thrust_returns_binary_signal(sample_market_data: pd.DataFrame) -> None:
31+
signal = DualThrustStrategy(k1=0.5, k2=0.5, window=5).generate_signals(sample_market_data)
32+
assert isinstance(signal, pd.Series)
33+
assert set(signal.dropna().unique()).issubset({0, 1})

0 commit comments

Comments
 (0)