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.
@@ -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
- # 1. Standard List: SP_100, SP_500, NASDAQ_100, NASDAQ_COMPOSITE, RUSSELL_1000,DOW_JONES_INDUSTRIALS, ETFS
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
- # 2. Custom List
95
+
96
+ # OPTION 2: Custom List - specify exactly which symbols you want to trade
29
97
  # universe = SecurityUniverseSelector.fromStockSymbols(['AAPL', 'MSFT'])
30
- # 3. Financial Query (Dynamic List)
31
- financialQuery = FinancialQuery() # MARKETCAP > 1B AND PE > 20
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 OnData to indicate when should this function be called and what values to pass
40
- This function is called separately for each security
41
- @WithData({
42
- "sma2": {"datatype": DataType.INDICATOR, "indicator": "SMA", "barinterval": BarInterval.ONE_MINUTE, "period": 2, "count": 2},
43
- "sma3": {"datatype": DataType.INDICATOR, "indicator": "SMA", "barinterval": BarInterval.ONE_MINUTE, "period": 3, "count": 2},
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
- "sma5": {"datatype": DataType.INDICATOR, "indicator": "SMA", "barinterval": BarInterval.ONE_MINUTE, "period": 2, "count": 2},
54
- "ema14": {"datatype": DataType.INDICATOR, "indicator": "EMA", "barinterval": BarInterval.ONE_MINUTE, "period": 14, "count": 2}
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
- :param security: The stock security against which this is evaluated. You use it to construct TradeSignal
59
- :param data: Dictionary with the requested data based on @DataParams annotation. The keys in the dictionary
60
- match the keys specified in @DataParams annotation ('sma5', and 'ema14' in this case)
61
- The data type of the value depends on datatype. Most common in DatedValue object which has two props: date and value
62
- datatype=INDICATOR, value type = DatedValue
63
- datatype=QUOTE, field is specified, value type = DatedValue
64
- datatype=QUOTE, field is not specified, value type is Quote object (has dayOpen, dayHigh, dayLow, prevOpen etc)
65
- datatype=BARS, value type is BAR
66
- Further, if the count is specified and greater than 1, value is returned as a List
67
- :return: TradeSignal if open condition matches and to signal open trade, None if open trade condition does not match
68
- """
69
-
70
- # We asked for latest two values for each of these indicators so that we can implement a "crossover"
71
- # semantics, i.e generate trade signal when sma2 crosses over ema14 (i.e sma5 was below ema14 at previous bar
72
- # but it is higher in this bar).
73
-
74
- # The other way to implement crossover effect is by storing previous result in state as described below
75
- sma5 = data["sma5"]
76
- ema14 = data["ema14"]
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
- # when current sma4 > ema14 and previous sma5 <= ema14, return a TradeSignal.
79
- # TradeSignal can optionally set TradeSignal.strength to indicate strength of the signal
80
- return TradeSignal(security, PositionType.LONG)
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
- In this method, you convert the TradeSignals into TradeOrders. You must do this for couple reasons:
87
- 1. Assume 1000 stocks match the open trade condition and so you have 1000 TradeSignals, but that does not
88
- mean that you want to open position for 1000 stocks in your portfolio. You may want to order those trade signals
89
- by strength and limit to top 10 trade signals
90
- 2. Your portfolio may already have open position for a stock corresponding to particular trade signal. In that case,
91
- you may wan to skip that trade signal, and prioritize opening new position for other stocks
92
- 3. Here, you also set TradeOrder speficiations such as order type, quantity etc
93
- 4. You may want to fully rebalance portfolio baseed on these new trade signals
94
- :param portfolio: Current portfolio state
95
- :param tradeSignals: Trade Signals correspoding to stocks matching open trade condition
96
- :return: List of TradeOrders to execute
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
- # We provide a convenience utility that allocates given percent (10% set below) of portfolio in the given stock
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, tradeSignals)
230
+ return portfolioAllocator.allocatePortfolio(portfolio, sorted_signals)
102
231
 
103
232
  def getStandardCloseCondition(self) -> StandardCloseCriteria | None:
104
- # TargetProfit, StopLoss, Timeout are standard close/exit criteria. TargetProfit and StopLoss are specified in percentages
105
- return StandardCloseCriteria(targetProfit=5, stopLoss=-5, trailingStop=None, timeOut=TimeDelta(10, TimeUnit.DAYS))
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
- "sma5": {"datatype": DataType.INDICATOR, "indicator": "SMA", "barinterval": BarInterval.ONE_MINUTE, "period": 2},
110
- "ema14": {"datatype": DataType.INDICATOR, "indicator": "EMA", "barinterval": BarInterval.ONE_MINUTE, "period": 14}
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
- Implementing this method is optional. But when implemented, it should be implemented similar to evaluateOpenTradeCondition
115
- :param openPos: The open position
116
- :param data: Requested data that corresponds to the open position's security symbol
117
- :return: TradeOrder if the position is supposed to be closed, None otherwise
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
- # Note that unlike in evalOpenTradeCondition, "count" is omitted for both sma5 and ema14 DataParams. When
121
- # count is omitted, it defaults to count=1, which means we will get a single DatedValue instead of List[DatedValue]
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
- else:
133
- return None
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
@@ -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
- h = Bar()
62
- h['symbol'] = b['symbol']
63
- h['date'] = b['date']
64
- h['barinterval'] = b['barinterval']
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
- h['open'] = (b['open'] + b['close'])/2
66
+ ha_open = (b['open'] + b['close'])/2
71
67
  else:
72
- h['open'] = (bars[i-1]['open'] + bars[i-1]['close'])/2
73
-
74
- h['high'] = max(b['high'], h['open'], h['close'])
75
- h['low'] = min(b['low'], h['open'], h['close'])
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