investfly-sdk 1.6__py3-none-any.whl → 1.8__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.
- investfly/models/Indicator.py +73 -1
- investfly/models/MarketData.py +25 -19
- investfly/models/ModelUtils.py +9 -6
- investfly/models/PortfolioModels.py +8 -6
- investfly/models/SecurityUniverseSelector.py +103 -27
- investfly/models/TradingStrategy.py +5 -1
- investfly/models/__init__.py +2 -1
- investfly/samples/strategies/MacdTrendFollowingStrategy.py +98 -0
- investfly/samples/strategies/RsiMeanReversionStrategy.py +163 -0
- investfly/samples/strategies/SmaCrossOverTemplate.py +284 -70
- investfly/utils/CommonUtils.py +22 -15
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/METADATA +30 -42
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/RECORD +18 -15
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/WHEEL +1 -1
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/top_level.txt +1 -0
- talib/__init__.pyi +185 -0
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/LICENSE.txt +0 -0
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
from typing import Dict, Any, List
|
2
|
+
|
3
|
+
from investfly.models import *
|
4
|
+
from investfly.utils import PercentBasedPortfolioAllocator
|
5
|
+
|
6
|
+
|
7
|
+
class RsiMeanReversionStrategy(TradingStrategy):
|
8
|
+
"""
|
9
|
+
A mean reversion strategy based on the RSI (Relative Strength Index) indicator.
|
10
|
+
|
11
|
+
This strategy:
|
12
|
+
1. Uses the RSI indicator to identify overbought and oversold conditions
|
13
|
+
2. Generates buy signals when RSI moves from below 30 (oversold) back above 30 (potential upward reversal)
|
14
|
+
3. Includes risk management with target profit, stop loss, and time-based exit criteria
|
15
|
+
4. Allocates portfolio to the top 5 stocks showing the strongest reversal signals
|
16
|
+
|
17
|
+
Note: This strategy operates on daily bars, so evaluateOpenTradeCondition is called
|
18
|
+
at most once per day when a new daily bar is available.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self) -> None:
|
22
|
+
"""
|
23
|
+
Initialize the strategy with state to track RSI values.
|
24
|
+
|
25
|
+
Since the strategy state can only contain primitive values (int, float, bool),
|
26
|
+
we use a flattened key structure to track which securities were previously oversold.
|
27
|
+
"""
|
28
|
+
super().__init__()
|
29
|
+
# The state starts empty and will be populated as we process securities
|
30
|
+
# We'll use keys in the format "oversold_{symbol}" with boolean values
|
31
|
+
|
32
|
+
def getSecurityUniverseSelector(self) -> SecurityUniverseSelector:
|
33
|
+
"""
|
34
|
+
Select the universe of securities to trade.
|
35
|
+
|
36
|
+
This strategy uses the S&P 100 stocks, which includes 100 of the largest
|
37
|
+
and most established companies in the U.S. market.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
SecurityUniverseSelector: A selector configured to use the S&P 100 stocks
|
41
|
+
"""
|
42
|
+
return SecurityUniverseSelector.fromStandardList(StandardSymbolsList.SP_100)
|
43
|
+
|
44
|
+
@DataParams({
|
45
|
+
# RSI with standard 14-period setting
|
46
|
+
"rsi": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.RSI, "barinterval": BarInterval.ONE_DAY, "period": 14, "count": 2},
|
47
|
+
# Get the latest daily bar to access volume and price data
|
48
|
+
"daily_bar": {"datatype": DataType.BARS, "barinterval": BarInterval.ONE_DAY, "count": 5}
|
49
|
+
})
|
50
|
+
def evaluateOpenTradeCondition(self, security: Security, data: Dict[str, Any]) -> TradeSignal | None:
|
51
|
+
"""
|
52
|
+
Generate a buy signal when RSI moves from oversold territory (below 30) back above 30,
|
53
|
+
indicating a potential upward reversal.
|
54
|
+
|
55
|
+
The signal strength is calculated based on the magnitude of the RSI change, volume,
|
56
|
+
and price action from the daily bars.
|
57
|
+
|
58
|
+
This method is called at most once per day when a new daily bar is available.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
security: The security (stock) being evaluated
|
62
|
+
data: Dictionary containing the requested data:
|
63
|
+
- "rsi": List of the last 2 values of the 14-period RSI
|
64
|
+
- "daily_bar": List of the last 5 daily bars
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
TradeSignal: A signal to open a long position if reversal condition is met
|
68
|
+
None: If no reversal is detected
|
69
|
+
"""
|
70
|
+
rsi = data["rsi"]
|
71
|
+
daily_bars = data["daily_bar"]
|
72
|
+
symbol = security.symbol
|
73
|
+
|
74
|
+
# Get current and previous RSI values
|
75
|
+
current_rsi = rsi[-1].value
|
76
|
+
previous_rsi = rsi[-2].value
|
77
|
+
|
78
|
+
# Get the latest daily bar for volume and price data
|
79
|
+
latest_bar = daily_bars[-1]
|
80
|
+
|
81
|
+
# Create a key for this symbol in our state
|
82
|
+
state_key = f"oversold_{symbol}"
|
83
|
+
|
84
|
+
# Check if the security was previously oversold (RSI below 30)
|
85
|
+
# If the key doesn't exist yet, default to False
|
86
|
+
was_oversold = bool(self.state.get(state_key, False))
|
87
|
+
|
88
|
+
# Update the oversold state
|
89
|
+
if previous_rsi < 30:
|
90
|
+
self.state[state_key] = True
|
91
|
+
elif current_rsi > 50: # Reset once RSI moves well above oversold
|
92
|
+
self.state[state_key] = False
|
93
|
+
|
94
|
+
# Generate buy signal if RSI was below 30 and is now moving above 30
|
95
|
+
if was_oversold and previous_rsi < 30 and current_rsi >= 30:
|
96
|
+
# Calculate signal strength based on multiple factors
|
97
|
+
|
98
|
+
# 1. RSI change - larger change indicates stronger momentum
|
99
|
+
rsi_change = current_rsi - previous_rsi
|
100
|
+
|
101
|
+
# 2. Volume - higher volume indicates stronger confirmation
|
102
|
+
volume = latest_bar.volume
|
103
|
+
|
104
|
+
# 3. Price action - calculate the percentage gain in the latest bar
|
105
|
+
price_change_pct = ((latest_bar.close - latest_bar.open) / latest_bar.open) * 100 if latest_bar.open != 0 else 0
|
106
|
+
|
107
|
+
# 4. Volume trend - compare current volume to average of previous bars
|
108
|
+
avg_volume = sum(bar.volume for bar in daily_bars[:-1]) / (len(daily_bars) - 1) if len(daily_bars) > 1 else volume
|
109
|
+
volume_ratio = volume / avg_volume if avg_volume > 0 else 1
|
110
|
+
|
111
|
+
# Combine factors for signal strength (adjust weights as needed)
|
112
|
+
# Higher RSI change, higher volume, and positive price action result in stronger signals
|
113
|
+
base_strength = rsi_change * (1 + max(0, price_change_pct) / 10)
|
114
|
+
volume_factor = volume_ratio * (volume / 1000000) # Normalize volume
|
115
|
+
|
116
|
+
signal_strength = base_strength * volume_factor
|
117
|
+
|
118
|
+
# Return a long position signal with the calculated strength
|
119
|
+
return TradeSignal(security, PositionType.LONG, signal_strength)
|
120
|
+
|
121
|
+
return None
|
122
|
+
|
123
|
+
def getStandardCloseCondition(self) -> StandardCloseCriteria:
|
124
|
+
"""
|
125
|
+
Define standard exit criteria for positions.
|
126
|
+
|
127
|
+
Mean reversion strategies typically have shorter holding periods and tighter
|
128
|
+
profit targets compared to trend following strategies.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
StandardCloseCriteria: The configured exit criteria
|
132
|
+
"""
|
133
|
+
return StandardCloseCriteria(
|
134
|
+
targetProfit=4, # Take profit at 4% gain
|
135
|
+
stopLoss=-2, # Stop loss at 2% loss
|
136
|
+
trailingStop=None, # No trailing stop for this strategy
|
137
|
+
timeOut=TimeDelta(5, TimeUnit.DAYS) # Exit after 5 trading days
|
138
|
+
)
|
139
|
+
|
140
|
+
def processOpenTradeSignals(self, portfolio: Portfolio, tradeSignals: List[TradeSignal]) -> List[TradeOrder]:
|
141
|
+
"""
|
142
|
+
Process trade signals and allocate the portfolio accordingly.
|
143
|
+
|
144
|
+
This method converts TradeSignals into actual TradeOrders, determining:
|
145
|
+
- Which signals to act on (if there are more signals than available capital)
|
146
|
+
- How much capital to allocate to each position
|
147
|
+
|
148
|
+
The strategy allocates to a maximum of 5 stocks with equal weight (20% each).
|
149
|
+
|
150
|
+
Args:
|
151
|
+
portfolio: The current portfolio state
|
152
|
+
tradeSignals: List of trade signals generated by evaluateOpenTradeCondition
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
List[TradeOrder]: Orders to execute based on the signals and portfolio allocation
|
156
|
+
"""
|
157
|
+
# Sort trade signals by strength in descending order
|
158
|
+
sorted_signals = sorted(tradeSignals, key=lambda signal: signal.strength if signal.strength is not None else 0, reverse=True)
|
159
|
+
|
160
|
+
# Use the PercentBasedPortfolioAllocator to allocate the portfolio
|
161
|
+
# Allocate to the top 5 stocks with equal weight (20% each)
|
162
|
+
portfolioAllocator = PercentBasedPortfolioAllocator(5)
|
163
|
+
return portfolioAllocator.allocatePortfolio(portfolio, sorted_signals)
|
@@ -1,6 +1,30 @@
|
|
1
1
|
# This is a self-documenting starter template to define custom trading strategy in Python Programming Language
|
2
2
|
# This code can be used as-it-is to try a new strategy
|
3
3
|
|
4
|
+
"""
|
5
|
+
SmaCrossOverTemplate - A comprehensive template for creating trading strategies with Investfly
|
6
|
+
|
7
|
+
This template demonstrates how to implement a complete trading strategy using the Investfly SDK.
|
8
|
+
It shows a moving average crossover strategy that generates buy signals when a shorter-period SMA
|
9
|
+
crosses above a longer-period EMA, indicating potential upward momentum.
|
10
|
+
|
11
|
+
Key components demonstrated:
|
12
|
+
1. Security universe selection (how to choose which stocks to trade)
|
13
|
+
2. Data request specification (how to request indicator data, bars, etc.)
|
14
|
+
3. Trade signal generation (when to enter trades)
|
15
|
+
4. Portfolio allocation (how to allocate capital across multiple signals)
|
16
|
+
5. Exit criteria (when to exit trades)
|
17
|
+
6. State management (how to track state between executions)
|
18
|
+
|
19
|
+
Important note on evaluation frequency:
|
20
|
+
- Strategies using DataType.QUOTE are evaluated on every price quote (multiple times per second)
|
21
|
+
- Strategies using DataType.BARS are evaluated when a new bar is available (based on barinterval)
|
22
|
+
- For day trading, prefer using ONE_MINUTE bars instead of quotes to reduce computational load
|
23
|
+
|
24
|
+
This template is designed to be educational and can be used as a starting point for
|
25
|
+
developing your own custom trading strategies.
|
26
|
+
"""
|
27
|
+
|
4
28
|
# Following two imports are required
|
5
29
|
from investfly.models import *
|
6
30
|
from investfly.utils import *
|
@@ -20,15 +44,61 @@ import pandas
|
|
20
44
|
|
21
45
|
# Create a class that extends TradingStrategy and implement 5 methods shown below
|
22
46
|
class SmaCrossOverTemplate(TradingStrategy):
|
47
|
+
"""
|
48
|
+
A template strategy demonstrating a moving average crossover system.
|
49
|
+
|
50
|
+
This strategy:
|
51
|
+
1. Uses a combination of SMA and EMA indicators with different periods
|
52
|
+
2. Generates buy signals when the shorter SMA crosses above the longer EMA
|
53
|
+
3. Includes risk management with profit targets and stop losses
|
54
|
+
4. Demonstrates how to implement custom exit conditions
|
55
|
+
|
56
|
+
Moving average crossovers are one of the most basic and widely used technical analysis techniques
|
57
|
+
for identifying trend changes. When a shorter-period moving average crosses above a longer-period
|
58
|
+
moving average, it often indicates the beginning of an uptrend.
|
59
|
+
|
60
|
+
Note: This strategy operates on one-minute bars, so evaluateOpenTradeCondition is called
|
61
|
+
at most once per minute when a new one-minute bar is available.
|
62
|
+
"""
|
63
|
+
|
64
|
+
def __init__(self) -> None:
|
65
|
+
"""
|
66
|
+
Initialize the strategy.
|
67
|
+
|
68
|
+
This method is called when the strategy is first created. You can use it to:
|
69
|
+
- Initialize any state variables you want to track between executions
|
70
|
+
- Set up any configuration parameters for the strategy
|
71
|
+
|
72
|
+
The state dictionary is automatically persisted between strategy runs.
|
73
|
+
"""
|
74
|
+
super().__init__()
|
75
|
+
# Initialize state dictionary if needed
|
76
|
+
# self.state = {"my_custom_state": {}}
|
23
77
|
|
24
78
|
def getSecurityUniverseSelector(self) -> SecurityUniverseSelector:
|
79
|
+
"""
|
80
|
+
Define the universe of securities to trade.
|
81
|
+
|
82
|
+
This method narrows down the set of securities against which to run the strategy logic.
|
83
|
+
There are three main approaches to selecting securities:
|
84
|
+
1. Standard lists (e.g., S&P 100, NASDAQ 100)
|
85
|
+
2. Custom lists (specific symbols you want to trade)
|
86
|
+
3. Financial queries (dynamic lists based on fundamental metrics)
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
SecurityUniverseSelector: A selector configured with your chosen securities
|
90
|
+
"""
|
25
91
|
# Narrow down the scope (or universe of stocks) against which to run this strategy. We support 3 options
|
26
|
-
|
92
|
+
|
93
|
+
# OPTION 1: Standard List: SP_100, SP_500, NASDAQ_100, NASDAQ_COMPOSITE, RUSSELL_1000, DOW_JONES_INDUSTRIALS, ETFS
|
27
94
|
# universe = SecurityUniverseSelector.fromStandardList(StandardSymbolsList.SP_100)
|
28
|
-
|
95
|
+
|
96
|
+
# OPTION 2: Custom List - specify exactly which symbols you want to trade
|
29
97
|
# universe = SecurityUniverseSelector.fromStockSymbols(['AAPL', 'MSFT'])
|
30
|
-
|
31
|
-
|
98
|
+
|
99
|
+
# OPTION 3: Financial Query (Dynamic List) - select stocks based on fundamental metrics
|
100
|
+
# This example selects stocks with market cap > $1B and P/E ratio > 20
|
101
|
+
financialQuery = FinancialQuery()
|
32
102
|
financialQuery.addCondition(FinancialCondition(FinancialField.MARKET_CAP, ComparisonOperator.GREATER_THAN, "1B"))
|
33
103
|
financialQuery.addCondition(FinancialCondition(FinancialField.PRICE_TO_EARNINGS_RATIO, ComparisonOperator.GREATER_THAN, "20"))
|
34
104
|
universe = SecurityUniverseSelector.fromFinancialQuery(financialQuery)
|
@@ -36,98 +106,242 @@ class SmaCrossOverTemplate(TradingStrategy):
|
|
36
106
|
|
37
107
|
|
38
108
|
"""
|
39
|
-
The function evaluateOpenTradeCondition below must be annotated with
|
40
|
-
This function is called separately for each security
|
41
|
-
|
42
|
-
|
43
|
-
|
109
|
+
The function evaluateOpenTradeCondition below must be annotated with DataParams to indicate what data is needed.
|
110
|
+
This function is called separately for each security whenever new data is available based on the requested data types.
|
111
|
+
|
112
|
+
IMPORTANT: The types of data you request determine how frequently this function is called:
|
113
|
+
- DataType.BARS: Called when a new bar is available (frequency depends on barinterval)
|
114
|
+
- DataType.INDICATOR: Called based on the underlying data the indicator uses
|
115
|
+
- DataType.FINANCIAL: Called when new financial data is available (typically infrequent)
|
116
|
+
|
117
|
+
AVOID using DataType.QUOTE in day trading strategies as it causes the function to be called
|
118
|
+
multiple times per second, which is computationally expensive.
|
119
|
+
|
120
|
+
The DataParams annotation specifies what data you need for your strategy. For example:
|
121
|
+
|
122
|
+
@DataParams({
|
123
|
+
"sma2": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.SMA, "barinterval": BarInterval.ONE_MINUTE, "period": 2, "count": 2},
|
124
|
+
"sma3": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.SMA, "barinterval": BarInterval.ONE_MINUTE, "period": 3, "count": 2},
|
44
125
|
"allOneMinBars": {"datatype": DataType.BARS, "barinterval": BarInterval.ONE_MINUTE},
|
45
126
|
"latestDailyBar": {"datatype": DataType.BARS, "barinterval": BarInterval.ONE_DAY, "count":1},
|
46
|
-
"quote": {"datatype": DataType.QUOTE},
|
47
|
-
"lastprice": {"datatype": DataType.QUOTE, "field": QuoteField.LASTPRICE},
|
48
|
-
"allFinancials": {"datatype": DataType.FINANCIAL},
|
49
127
|
"revenue": {"datatype": DataType.FINANCIAL, "field": FinancialField.REVENUE}
|
50
128
|
})
|
129
|
+
|
130
|
+
The keys in the dictionary become the keys in the 'data' parameter passed to the function.
|
51
131
|
"""
|
52
132
|
@DataParams({
|
53
|
-
|
54
|
-
"
|
133
|
+
# Request 2-period SMA data for the last 2 data points to detect crossover
|
134
|
+
"sma5": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.SMA, "barinterval": BarInterval.ONE_MINUTE, "period": 2, "count": 2},
|
135
|
+
# Request 14-period EMA data for the last 2 data points to detect crossover
|
136
|
+
"ema14": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.EMA, "barinterval": BarInterval.ONE_MINUTE, "period": 14, "count": 2},
|
137
|
+
# Request the latest one-minute bar to get price and volume data for signal strength calculation
|
138
|
+
"minute_bar": {"datatype": DataType.BARS, "barinterval": BarInterval.ONE_MINUTE, "count": 1}
|
55
139
|
})
|
56
140
|
def evaluateOpenTradeCondition(self, security: Security, data: Dict[str, Any]) -> TradeSignal | None:
|
57
141
|
"""
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
142
|
+
Evaluate whether to open a trade for a given security.
|
143
|
+
|
144
|
+
This method is called for each security in the universe whenever a new one-minute bar is available.
|
145
|
+
It analyzes the requested data to determine if a trade signal should be generated.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
security: The security (stock) being evaluated
|
149
|
+
data: Dictionary containing the requested data based on @DataParams annotation
|
150
|
+
The keys match those specified in the DataParams decorator
|
151
|
+
|
152
|
+
Data types in the 'data' dictionary:
|
153
|
+
- datatype=INDICATOR: value is DatedValue (or List[DatedValue] if count > 1)
|
154
|
+
- datatype=BARS: value is Bar (or List[Bar] if count > 1)
|
155
|
+
- datatype=FINANCIAL: value depends on the specific financial data requested
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
TradeSignal: A signal to open a position if conditions are met
|
159
|
+
None: If conditions for opening a trade are not met
|
160
|
+
"""
|
161
|
+
# Extract the indicator data from the data dictionary
|
162
|
+
sma5 = data["sma5"] # List of the last 2 values of 2-period SMA
|
163
|
+
ema14 = data["ema14"] # List of the last 2 values of 14-period EMA
|
164
|
+
minute_bar = data["minute_bar"][-1] # Get the latest one-minute bar
|
165
|
+
|
166
|
+
# Implement crossover detection logic:
|
167
|
+
# We generate a trade signal when the SMA crosses above the EMA
|
168
|
+
# This happens when:
|
169
|
+
# 1. Current SMA is above current EMA (sma5[-1].value > ema14[-1].value)
|
170
|
+
# 2. Previous SMA was below or equal to previous EMA (sma5[-2].value <= ema14[-2].value)
|
77
171
|
if sma5[-1].value > ema14[-1].value and sma5[-2].value <= ema14[-2].value:
|
78
|
-
#
|
79
|
-
|
80
|
-
|
172
|
+
# Calculate signal strength based on multiple factors:
|
173
|
+
|
174
|
+
# Get the closing price and volume from the latest bar
|
175
|
+
closing_price = minute_bar.close
|
176
|
+
volume = minute_bar.volume
|
177
|
+
|
178
|
+
# 1. Calculate the crossover magnitude as a percentage
|
179
|
+
crossover_magnitude = ((sma5[-1].value - ema14[-1].value) / ema14[-1].value) * 100
|
180
|
+
|
181
|
+
# 2. Calculate the rate of change of the shorter SMA to measure momentum
|
182
|
+
sma_change = ((sma5[-1].value - sma5[-2].value) / sma5[-2].value) * 100 if sma5[-2].value != 0 else 0
|
183
|
+
|
184
|
+
# 3. Consider the bar's price movement (close vs open)
|
185
|
+
price_change_pct = ((minute_bar.close - minute_bar.open) / minute_bar.open) * 100 if minute_bar.open != 0 else 0
|
186
|
+
|
187
|
+
# Combine factors for signal strength (adjust weights as needed)
|
188
|
+
# Higher volume, larger crossover magnitude, and positive price movement result in stronger signals
|
189
|
+
signal_strength = (crossover_magnitude * 0.4) + (sma_change * 0.3) + (price_change_pct * 0.3)
|
190
|
+
|
191
|
+
# Apply volume as a multiplier (normalized)
|
192
|
+
signal_strength = signal_strength * (1 + (volume / 100000))
|
193
|
+
|
194
|
+
# Crossover detected - generate a long position signal with calculated strength
|
195
|
+
return TradeSignal(security, PositionType.LONG, signal_strength)
|
81
196
|
else:
|
197
|
+
# No crossover - return None (no trade signal)
|
82
198
|
return None
|
83
199
|
|
84
200
|
def processOpenTradeSignals(self, portfolio: Portfolio, tradeSignals: List[TradeSignal]) -> List[TradeOrder]:
|
85
201
|
"""
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
202
|
+
Process trade signals and allocate the portfolio accordingly.
|
203
|
+
|
204
|
+
This method is called after evaluateOpenTradeCondition has been called for all securities
|
205
|
+
in the universe. It receives a list of all the trade signals generated and must convert
|
206
|
+
them into actual trade orders.
|
207
|
+
|
208
|
+
This is where you implement your portfolio allocation logic, such as:
|
209
|
+
1. Limiting the number of positions (e.g., only take the top 10 signals)
|
210
|
+
2. Allocating different amounts to different positions
|
211
|
+
3. Handling existing positions in the portfolio
|
212
|
+
4. Setting order types, quantities, and other specifications
|
213
|
+
|
214
|
+
Args:
|
215
|
+
portfolio: The current portfolio state, including:
|
216
|
+
- Current cash balance
|
217
|
+
- Open positions
|
218
|
+
- Pending orders
|
219
|
+
tradeSignals: List of trade signals generated by evaluateOpenTradeCondition
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
List[TradeOrder]: Orders to execute based on the signals and portfolio allocation
|
97
223
|
"""
|
98
|
-
|
99
|
-
|
224
|
+
# Sort trade signals by strength in descending order to prioritize stronger signals
|
225
|
+
sorted_signals = sorted(tradeSignals, key=lambda signal: signal.strength if signal.strength is not None else 0, reverse=True)
|
226
|
+
|
227
|
+
# We provide a convenience utility that allocates a given percent of the portfolio to each position
|
228
|
+
# In this case, we allocate 10% of the portfolio to each position (max 10 positions)
|
100
229
|
portfolioAllocator = PercentBasedPortfolioAllocator(10)
|
101
|
-
return portfolioAllocator.allocatePortfolio(portfolio,
|
230
|
+
return portfolioAllocator.allocatePortfolio(portfolio, sorted_signals)
|
102
231
|
|
103
232
|
def getStandardCloseCondition(self) -> StandardCloseCriteria | None:
|
104
|
-
|
105
|
-
|
233
|
+
"""
|
234
|
+
Define standard exit criteria for positions.
|
235
|
+
|
236
|
+
This method specifies when to automatically close positions based on:
|
237
|
+
- Profit target: Close when position gains a certain percentage
|
238
|
+
- Stop loss: Close when position loses a certain percentage
|
239
|
+
- Trailing stop: Close when position retraces a certain percentage from its peak
|
240
|
+
- Time limit: Close after a certain amount of time regardless of performance
|
241
|
+
|
242
|
+
These are considered "standard" exit conditions that are commonly used.
|
243
|
+
For more complex exit conditions, implement the evaluateCloseTradeCondition method.
|
244
|
+
|
245
|
+
Returns:
|
246
|
+
StandardCloseCriteria: The configured exit criteria
|
247
|
+
None: If you don't want to use standard exit criteria
|
248
|
+
"""
|
249
|
+
# TargetProfit and StopLoss are specified in percentages (5% and -5%)
|
250
|
+
# TimeOut specifies a maximum holding period (10 days in this case)
|
251
|
+
return StandardCloseCriteria(
|
252
|
+
targetProfit=5, # Take profit at 5% gain
|
253
|
+
stopLoss=-5, # Stop loss at 5% loss
|
254
|
+
trailingStop=None, # No trailing stop for this strategy
|
255
|
+
timeOut=TimeDelta(10, TimeUnit.DAYS) # Exit after 10 days
|
256
|
+
)
|
106
257
|
|
107
258
|
|
108
259
|
@DataParams({
|
109
|
-
|
110
|
-
"
|
260
|
+
# Request current SMA value (no need for history since we're not detecting crossover)
|
261
|
+
"sma5": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.SMA, "barinterval": BarInterval.ONE_MINUTE, "period": 2},
|
262
|
+
# Request current EMA value
|
263
|
+
"ema14": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.EMA, "barinterval": BarInterval.ONE_MINUTE, "period": 14},
|
264
|
+
# Request the latest one-minute bar for price information
|
265
|
+
"minute_bar": {"datatype": DataType.BARS, "barinterval": BarInterval.ONE_MINUTE, "count": 1}
|
111
266
|
})
|
112
267
|
def evaluateCloseTradeCondition(self, openPos: OpenPosition, data: Dict[str, Any]) -> TradeOrder | None:
|
113
268
|
"""
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
269
|
+
Evaluate whether to close an existing position.
|
270
|
+
|
271
|
+
This method is called for each open position whenever new data is available.
|
272
|
+
It allows you to implement custom exit conditions beyond the standard criteria.
|
273
|
+
|
274
|
+
Note: This method is optional. If you don't implement it, only the standard
|
275
|
+
close conditions specified in getStandardCloseCondition will be used.
|
276
|
+
|
277
|
+
Args:
|
278
|
+
openPos: The open position being evaluated, including:
|
279
|
+
- The security
|
280
|
+
- Entry price
|
281
|
+
- Position type (LONG or SHORT)
|
282
|
+
- Quantity
|
283
|
+
- Open date
|
284
|
+
data: Dictionary containing the requested data based on @DataParams annotation
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
TradeOrder: An order to close the position if conditions are met
|
288
|
+
None: If conditions for closing the position are not met
|
118
289
|
"""
|
290
|
+
# Note that unlike in evaluateOpenTradeCondition, we didn't specify "count" for the indicators
|
291
|
+
# When count is omitted, it defaults to count=1, which means we get a single DatedValue
|
292
|
+
# instead of a List[DatedValue]
|
293
|
+
|
294
|
+
# Extract the indicator data
|
295
|
+
sma5 = data["sma5"] # Current value of 2-period SMA (a single DatedValue)
|
296
|
+
ema14 = data["ema14"] # Current value of 14-period EMA (a single DatedValue)
|
297
|
+
minute_bar = data["minute_bar"][-1] # Get the latest one-minute bar
|
119
298
|
|
120
|
-
#
|
121
|
-
#
|
122
|
-
|
123
|
-
# For close conditions, implementing crossover effect is not required because the first time the condition
|
124
|
-
# defined below is met and TradeOrder is returned to close the position, closeOrder will be submitted and this
|
125
|
-
# method is never called on the same open position again
|
126
|
-
|
127
|
-
sma5 = data["sma5"]
|
128
|
-
ema14 = data["ema14"]
|
129
|
-
|
299
|
+
# Custom exit condition: Close the position if SMA falls below 90% of EMA
|
300
|
+
# This might indicate that the uptrend is weakening
|
130
301
|
if sma5.value < 0.9 * ema14.value:
|
302
|
+
# Create a sell order to close the position
|
131
303
|
return TradeOrder(openPos.security, TradeType.SELL)
|
132
|
-
|
133
|
-
|
304
|
+
|
305
|
+
# Additional exit condition: Close if the current bar shows significant price weakness
|
306
|
+
# (e.g., if the close is near the low of the bar and significantly below the open)
|
307
|
+
bar_range = minute_bar.high - minute_bar.low
|
308
|
+
if bar_range > 0:
|
309
|
+
# Calculate how close the close is to the low (0 = at low, 1 = at high)
|
310
|
+
close_to_low_ratio = (minute_bar.close - minute_bar.low) / bar_range
|
311
|
+
|
312
|
+
# Calculate the percentage drop from open to close
|
313
|
+
open_to_close_drop = ((minute_bar.open - minute_bar.close) / minute_bar.open) * 100 if minute_bar.open != 0 else 0
|
314
|
+
|
315
|
+
# Exit if close is in bottom 20% of the bar range and there's a significant drop from open
|
316
|
+
if close_to_low_ratio < 0.2 and open_to_close_drop > 0.5:
|
317
|
+
return TradeOrder(openPos.security, TradeType.SELL)
|
318
|
+
|
319
|
+
# Conditions not met - don't close the position
|
320
|
+
return None
|
321
|
+
|
322
|
+
# Optional methods for state management
|
323
|
+
|
324
|
+
def getState(self) -> Dict[str, Any]:
|
325
|
+
"""
|
326
|
+
Return the current state to be persisted between strategy runs.
|
327
|
+
|
328
|
+
This method is called to save the strategy's state between executions.
|
329
|
+
The returned dictionary will be passed to restoreState when the strategy
|
330
|
+
is next executed.
|
331
|
+
|
332
|
+
Returns:
|
333
|
+
Dict[str, Any]: The state to be persisted
|
334
|
+
"""
|
335
|
+
return self.state
|
336
|
+
|
337
|
+
def restoreState(self, state: Dict[str, Any]) -> None:
|
338
|
+
"""
|
339
|
+
Restore the strategy state from persisted data.
|
340
|
+
|
341
|
+
This method is called when the strategy is executed, passing in the state
|
342
|
+
that was previously returned by getState.
|
343
|
+
|
344
|
+
Args:
|
345
|
+
state: The previously saved state
|
346
|
+
"""
|
347
|
+
self.state = state
|
investfly/utils/CommonUtils.py
CHANGED
@@ -46,7 +46,7 @@ def createListOfDatedValue(dates: List[datetime], values: numpy.ndarray[Any, num
|
|
46
46
|
result.append(DatedValue(date, val.item()))
|
47
47
|
return result
|
48
48
|
|
49
|
-
def floatListToDatedValueList(dates: List[datetime], values: List[float]):
|
49
|
+
def floatListToDatedValueList(dates: List[datetime], values: List[float|int]):
|
50
50
|
result: List[DatedValue] = []
|
51
51
|
for i in range(len(dates)):
|
52
52
|
date: datetime = dates[i]
|
@@ -58,22 +58,29 @@ def toHeikinAshi(bars: List[Bar]) -> List[Bar]:
|
|
58
58
|
heiken: List[Bar] = []
|
59
59
|
for i in range(len(bars)):
|
60
60
|
b = bars[i]
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
h['volume'] = b['volume']
|
66
|
-
|
67
|
-
h['close'] = (b['open'] + b['high'] + b['low'] + b['close']) / 4
|
68
|
-
|
61
|
+
|
62
|
+
# Calculate Heikin-Ashi values
|
63
|
+
ha_close = (b['open'] + b['high'] + b['low'] + b['close']) / 4
|
64
|
+
|
69
65
|
if i == 0:
|
70
|
-
|
66
|
+
ha_open = (b['open'] + b['close'])/2
|
71
67
|
else:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
68
|
+
ha_open = (heiken[i-1]['open'] + heiken[i-1]['close'])/2
|
69
|
+
|
70
|
+
ha_high = max(b['high'], ha_open, ha_close)
|
71
|
+
ha_low = min(b['low'], ha_open, ha_close)
|
72
|
+
|
73
|
+
h = Bar(
|
74
|
+
symbol=b['symbol'],
|
75
|
+
date=b['date'],
|
76
|
+
barinterval=b['barinterval'],
|
77
|
+
volume=b['volume'],
|
78
|
+
open=ha_open,
|
79
|
+
close=ha_close,
|
80
|
+
high=ha_high,
|
81
|
+
low=ha_low
|
82
|
+
)
|
83
|
+
|
77
84
|
heiken.append(h)
|
78
85
|
|
79
86
|
return heiken
|