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.
@@ -27,6 +27,78 @@ class ParamType(str, Enum):
27
27
  return self.value
28
28
 
29
29
 
30
+ class INDICATORS(str, Enum):
31
+ """
32
+ Enum listing all supported technical indicators in the system.
33
+ These indicators can be used in trading strategies for technical analysis.
34
+ """
35
+
36
+ SMA = 'SMA' # Simple Moving Average
37
+ EMA = 'EMA' # Exponential Moving Average
38
+ TEMA = 'TEMA' # Triple Exponential Moving Average
39
+ DEMA = 'DEMA' # Double Exponential Moving Average
40
+ KAMA = 'KAMA' # Kaufman Adaptive Moving Average
41
+ MAMA = 'MAMA' # MESA Adaptive Moving Average
42
+ FAMA = 'FAMA' # Following Adaptive Moving Average
43
+ UPPERBBAND = 'UPPERBBAND' # Upper Bollinger Band
44
+ LOWERBBAND = 'LOWERBBAND' # Lower Bollinger Band
45
+ ICHIMOKU = 'ICHIMOKU' # Ichimoku Conversion Line
46
+ KELTNER = 'KELTNER' # Keltner Channel Middle Line
47
+ MACD = 'MACD' # Moving Average Convergence/Divergence
48
+ MACDS = 'MACDS' # MACD Signal Line
49
+ RSI = 'RSI' # Relative Strength Index
50
+ ROC = 'ROC' # Rate of Change
51
+ CCI = 'CCI' # Commodity Channel Index
52
+ ADX = 'ADX' # Average Directional Index
53
+ ADXR = 'ADXR' # Average Directional Movement Index Rating
54
+ AROONOSC = 'AROONOSC' # Aroon Oscillator
55
+ AROON = 'AROON' # Aroon Up
56
+ AROONDOWN = 'AROONDOWN' # Aroon Down
57
+ MFI = 'MFI' # Money Flow Index
58
+ CMO = 'CMO' # Chande Momentum Oscillator
59
+ STOCH = 'STOCH' # Stochastic
60
+ STOCHF = 'STOCHF' # Stochastic Fast
61
+ STOCHRSI = 'STOCHRSI' # Stochastic RSI
62
+ APO = 'APO' # Absolute Price Oscillator
63
+ PPO = 'PPO' # Percentage Price Oscillator
64
+ MINUS_DI = 'MINUS_DI' # Minus Directional Indicator
65
+ PLUS_DI = 'PLUS_DI' # Plus Directional Indicator
66
+ DX = 'DX' # Directional Movement Index
67
+ TRIX = 'TRIX' # Triple Exponential Moving Average Oscillator
68
+ BOP = 'BOP' # Balance of Power
69
+ OBV = 'OBV' # On Balance Volume
70
+ CMF = 'CMF' # Chaikin Money Flow
71
+ AVGVOL = 'AVGVOL' # Average Volume
72
+ ATR = 'ATR' # Average True Range
73
+ AVGPRICE = 'AVGPRICE' # Average Price
74
+ MEDPRICE = 'MEDPRICE' # Median Price
75
+ TYPPRICE = 'TYPPRICE' # Typical Price
76
+ WCLPRICE = 'WCLPRICE' # Weighted Close Price
77
+ BARPRICE = 'BARPRICE' # Bar Price (custom)
78
+ MAX = 'MAX' # Maximum value over period
79
+ MIN = 'MIN' # Minimum value over period
80
+ CDLENGULFING = 'CDLENGULFING' # Engulfing Pattern
81
+ CDLDOJI = 'CDLDOJI' # Doji
82
+ CDLHAMMER = 'CDLHAMMER' # Hammer
83
+ CDLMORNINGSTAR = 'CDLMORNINGSTAR' # Morning Star
84
+ CDLEVENINGSTAR = 'CDLEVENINGSTAR' # Evening Star
85
+ CDLHARAMI = 'CDLHARAMI' # Harami Pattern
86
+ CDLSHOOTINGSTAR = 'CDLSHOOTINGSTAR' # Shooting Star
87
+ CDL3BLACKCROWS = 'CDL3BLACKCROWS' # Three Black Crows
88
+ CDL3WHITESOLDIERS = 'CDL3WHITESOLDIERS' # Three White Soldiers
89
+ CDLMARUBOZU = 'CDLMARUBOZU' # Marubozu
90
+ PSAR = 'PSAR' # Parabolic SAR
91
+ WILLIAMR = 'WILLIAMR' # Williams' %R
92
+ VWAP = 'VWAP' # Volume Weighted Average Price
93
+ RVOL = 'RVOL' # Relative Volume
94
+
95
+ def __str__(self):
96
+ return self.value
97
+
98
+ def __repr__(self):
99
+ return self.value
100
+
101
+
30
102
  @dataclass
31
103
  class IndicatorParamSpec:
32
104
 
@@ -46,7 +118,7 @@ class IndicatorParamSpec:
46
118
  """ Valid value options (if any). If specified, then in the UI, this parameter renders as a dropdown select list.
47
119
  If left as None, parameter renders and freeform input text field. """
48
120
 
49
- PERIOD_VALUES: ClassVar[List[int]] = [2, 3, 4, 5, 8, 9, 10, 12, 14, 15, 20, 26, 30, 40, 50, 60, 70, 80, 90, 100, 120, 130, 140, 150, 180, 200, 250, 300]
121
+ PERIOD_VALUES: ClassVar[List[int]] = [2, 3, 4, 5, 6, 7, 8,9, 10, 12, 14, 15, 20, 24, 26, 30, 40, 50, 60, 70, 80, 90, 100, 120, 130, 140, 150, 180, 200, 250, 300]
50
122
 
51
123
  def toDict(self) -> Dict[str, Any]:
52
124
  d = self.__dict__.copy()
@@ -23,6 +23,8 @@ class SecurityType(str, Enum):
23
23
 
24
24
  STOCK = "STOCK"
25
25
  ETF = "ETF"
26
+ CRYPTO = "CRYPTO"
27
+ FOREX = "FOREX"
26
28
 
27
29
  def __str__(self):
28
30
  return self.value
@@ -65,21 +67,22 @@ class Quote:
65
67
  lastPrice: float
66
68
  prevClose: float| None = None
67
69
  todayChange: float | None = None
68
- todayChangePercent: float | None = None
70
+ todayChangePct: float | None = None
69
71
  dayOpen: float | None = None
70
72
  dayHigh: float | None = None
71
73
  dayLow: float | None = None
72
- dayVolume: int | None = None
74
+ volume: int | None = None
73
75
 
74
76
  @staticmethod
75
77
  def fromDict(jsonDict: Dict[str, Any]) -> Quote:
76
78
  quote = Quote(jsonDict["symbol"], parseDatetime(jsonDict["date"]), jsonDict["lastPrice"])
77
79
  quote.prevClose = jsonDict.get('prevClose')
78
80
  quote.todayChange = jsonDict.get("todayChange")
79
- quote.todayChangePercent = jsonDict.get('todayChangePercent')
81
+ quote.todayChangePct = jsonDict.get('todayChangePct')
80
82
  quote.dayOpen = jsonDict.get('dayOpen')
81
83
  quote.dayHigh = jsonDict.get('dayHigh')
82
84
  quote.dayLow = jsonDict.get('dayLow')
85
+ quote.volume = jsonDict.get('volume')
83
86
  return quote
84
87
 
85
88
  def toDict(self) -> Dict[str, Any]:
@@ -111,27 +114,31 @@ class Quote:
111
114
  raise Exception("DAY_CHANGE not available in Quote")
112
115
  return DatedValue(self.date, self.todayChange)
113
116
  elif quoteField == QuoteField.DAY_CHANGE_PCT:
114
- if self.todayChangePercent is None:
117
+ if self.todayChangePct is None:
115
118
  raise Exception("DAY_CHANGE_PCT value not available in Quote")
116
- return DatedValue(self.date, self.todayChangePercent)
119
+ return DatedValue(self.date, self.todayChangePct)
117
120
  elif quoteField == QuoteField.DAY_VOLUME:
118
- if self.dayVolume is None:
121
+ if self.volume is None:
119
122
  raise Exception("DAY_VOLUME not available in Quote")
120
- return DatedValue(self.date, self.dayVolume)
123
+ return DatedValue(self.date, self.volume)
124
+ elif quoteField == QuoteField.DAY_CHANGE_OPEN:
125
+ if self.dayOpen is None or self.lastPrice is None:
126
+ raise Exception("DAY_CHANGE_OPEN not available in Quote")
127
+ return DatedValue(self.date, self.lastPrice - self.dayOpen)
121
128
  else:
122
129
  raise Exception("Invalid Quote Indicator ID: " + quoteField)
123
130
 
124
131
  def toEODBar(self) -> Bar:
125
- bar = Bar()
126
- bar['symbol'] = self.symbol
127
- bar['barinterval'] = BarInterval.ONE_DAY
128
- bar['date'] = self.date.replace(second=0, microsecond=0)
129
- bar['open'] = cast(float, self.dayOpen)
130
- bar['high'] = cast(float, self.dayHigh)
131
- bar['low'] = cast(float, self.dayLow)
132
- bar['close'] = cast(float, self.lastPrice)
133
- bar['volume'] = cast(int, self.dayVolume)
134
- return bar
132
+ return Bar(
133
+ symbol=self.symbol,
134
+ barinterval=BarInterval.ONE_DAY,
135
+ date=self.date.replace(second=0, microsecond=0),
136
+ open=cast(float, self.dayOpen),
137
+ high=cast(float, self.dayHigh),
138
+ low=cast(float, self.dayLow),
139
+ close=cast(float, self.lastPrice),
140
+ volume=cast(int, self.volume)
141
+ )
135
142
 
136
143
 
137
144
  class BarInterval(str, Enum):
@@ -167,8 +174,7 @@ Bar = TypedDict("Bar", {"symbol": str,
167
174
  "high": float,
168
175
  "low": float,
169
176
  "volume": int
170
- },
171
- total=False)
177
+ })
172
178
 
173
179
  @dataclass
174
180
  class StockNews:
@@ -1,23 +1,26 @@
1
1
  from datetime import datetime
2
- import pytz
3
-
2
+ from zoneinfo import ZoneInfo
4
3
 
5
4
  class ModelUtils:
6
5
 
7
6
  # In Java, using Date includes timezone offset in JSON whereas using LocalDateTime does not
8
7
 
9
- est_tz = pytz.timezone('America/New_York')
8
+ est_zone = ZoneInfo("US/Eastern")
9
+
10
+ @staticmethod
11
+ def convertoToEst(dt: datetime) -> datetime:
12
+ return dt.astimezone(ModelUtils.est_zone)
10
13
 
11
14
  @staticmethod
12
15
  def localizeDateTime(dt: datetime) -> datetime:
13
- return ModelUtils.est_tz.localize(dt)
16
+ return dt.replace(tzinfo=ModelUtils.est_zone)
14
17
 
15
18
 
16
19
  @staticmethod
17
20
  def parseDatetime(date_str: str) -> datetime:
18
21
  dateFormat = '%Y-%m-%dT%H:%M:%S.%f' if "." in date_str else '%Y-%m-%dT%H:%M:%S'
19
22
  dt = datetime.strptime(date_str, dateFormat)
20
- dt = dt.astimezone(ModelUtils.est_tz)
23
+ dt = dt.astimezone(ModelUtils.est_zone)
21
24
  return dt
22
25
 
23
26
  @staticmethod
@@ -27,7 +30,7 @@ class ModelUtils:
27
30
  @staticmethod
28
31
  def parseZonedDatetime(date_str: str) -> datetime:
29
32
  dt = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S.%f%z')
30
- dt = dt.astimezone(ModelUtils.est_tz)
33
+ dt = dt.astimezone(ModelUtils.est_zone)
31
34
  return dt
32
35
 
33
36
  @staticmethod
@@ -52,10 +52,12 @@ class Broker(str, Enum):
52
52
 
53
53
  """Broker Type Enum"""
54
54
 
55
- INVESTFLY = "INVESTFLY"
56
55
  TRADIER = "TRADIER"
57
- TDAMERITRADE = "TDAMERITRADE"
56
+ INVESTFLY = "INVESTFLY"
57
+ TASTYTRADE = "TASTYTRADE"
58
+ ALPACA = "ALPACA"
58
59
  BACKTEST = "BACKTEST"
60
+ OANDA = "OANDA"
59
61
 
60
62
  def __str__(self):
61
63
  return self.value
@@ -70,7 +72,7 @@ class TradeOrder:
70
72
  security: Security
71
73
  tradeType: TradeType
72
74
  orderType: OrderType = OrderType.MARKET_ORDER
73
- quantity: int | None = None
75
+ quantity: float | None = None
74
76
  maxAmount: float | None = None
75
77
  limitPrice: float | None = None # If left empty, will use latest quote as limit price
76
78
 
@@ -133,7 +135,7 @@ class CompletedTrade:
133
135
  security: Security
134
136
  date: datetime
135
137
  price: float
136
- quantity: int
138
+ quantity: float
137
139
  tradeType: TradeType
138
140
 
139
141
  @staticmethod
@@ -151,7 +153,7 @@ class ClosedPosition:
151
153
  closeDate: datetime
152
154
  openPrice: float
153
155
  closePrice: float
154
- quantity: int
156
+ quantity: float
155
157
  profitLoss: float|None = None
156
158
  percentChange: float| None = None
157
159
 
@@ -186,7 +188,7 @@ class OpenPosition:
186
188
  security: Security
187
189
  position: PositionType
188
190
  avgPrice: float
189
- quantity: int
191
+ quantity: float
190
192
  purchaseDate: datetime
191
193
  currentPrice: float | None = None
192
194
  currentValue: float | None = None
@@ -5,19 +5,49 @@ from enum import Enum
5
5
  from numbers import Number
6
6
  from typing import List, Dict, Any, cast
7
7
 
8
- from investfly.models.MarketData import SecurityType
8
+ from investfly.models.MarketData import SecurityType, Security
9
9
  from investfly.models.MarketDataIds import FinancialField
10
10
 
11
11
 
12
12
  class StandardSymbolsList(str, Enum):
13
+ # Stock lists
13
14
  SP_100 = "SP_100"
14
15
  SP_500 = "SP_500"
15
16
  NASDAQ_100 = "NASDAQ_100"
16
17
  NASDAQ_COMPOSITE = "NASDAQ_COMPOSITE"
17
18
  RUSSELL_1000 = "RUSSELL_1000"
19
+ RUSSELL_2000 = "RUSSELL_2000"
18
20
  DOW_JONES_INDUSTRIALS = "DOW_JONES_INDUSTRIALS"
21
+
22
+ # General lists
23
+ STOCKS = "STOCKS"
19
24
  ETFS = "ETFS"
20
25
 
26
+ ALL_CRYPTO = "ALL_CRYPTO"
27
+ USD_CRYPTO = "USD_CRYPTO"
28
+
29
+ # Forex lists
30
+ ALL_FOREX = "ALL_FOREX"
31
+
32
+ @property
33
+ def securityType(self) -> SecurityType:
34
+ # Map each enum value to its corresponding security type
35
+ security_type_map = {
36
+ "SP_100": SecurityType.STOCK,
37
+ "SP_500": SecurityType.STOCK,
38
+ "NASDAQ_100": SecurityType.STOCK,
39
+ "NASDAQ_COMPOSITE": SecurityType.STOCK,
40
+ "RUSSELL_1000": SecurityType.STOCK,
41
+ "RUSSELL_2000": SecurityType.STOCK,
42
+ "DOW_JONES_INDUSTRIALS": SecurityType.STOCK,
43
+ "STOCKS": SecurityType.STOCK,
44
+ "ETFS": SecurityType.STOCK,
45
+ "ALL_CRYPTO": SecurityType.CRYPTO,
46
+ "USD_CRYPTO": SecurityType.CRYPTO,
47
+ "ALL_FOREX": SecurityType.FOREX
48
+ }
49
+ return security_type_map[self.value]
50
+
21
51
  def __str__(self):
22
52
  return self.value
23
53
 
@@ -26,8 +56,7 @@ class StandardSymbolsList(str, Enum):
26
56
 
27
57
 
28
58
  class CustomSecurityList:
29
- def __init__(self, securityType: SecurityType):
30
- self.securityType = securityType
59
+ def __init__(self):
31
60
  self.symbols: List[str] = []
32
61
 
33
62
  def addSymbol(self, symbol: str) -> None:
@@ -35,7 +64,7 @@ class CustomSecurityList:
35
64
 
36
65
  @staticmethod
37
66
  def fromJson(json_dict: Dict[str, Any]) -> CustomSecurityList:
38
- securityList = CustomSecurityList(SecurityType(json_dict['securityType']))
67
+ securityList = CustomSecurityList()
39
68
  securityList.symbols = json_dict['symbols']
40
69
  return securityList
41
70
 
@@ -43,8 +72,6 @@ class CustomSecurityList:
43
72
  return self.__dict__.copy()
44
73
 
45
74
  def validate(self) -> None:
46
- if self.securityType is None:
47
- raise Exception("CustomSecurityList.securityType is required")
48
75
  if len(self.symbols) == 0:
49
76
  raise Exception("CustomSecurityList.symbols: At least one symbol is required")
50
77
 
@@ -133,6 +160,10 @@ class SecurityUniverseSelector:
133
160
  You can pick one of the standard list (e.g SP100) that we provide, provide your own list with comma separated symbols list,
134
161
  or provide a query based on fundamental metrics like MarketCap, PE Ratio etc.
135
162
  """
163
+
164
+ securityType: SecurityType
165
+ """The security type for the universe selector"""
166
+
136
167
  universeType: SecurityUniverseType
137
168
  """The approach used to specify the stocks. Depending on the universeType, one of the attribute below must be specified"""
138
169
 
@@ -142,16 +173,35 @@ class SecurityUniverseSelector:
142
173
  customList: CustomSecurityList | None = None
143
174
  financialQuery: FinancialQuery | None = None
144
175
 
176
+ @staticmethod
177
+ def getValidSymbolLists(securityType: SecurityType) -> List[StandardSymbolsList]:
178
+ if securityType == SecurityType.CRYPTO:
179
+ return [StandardSymbolsList.USD_CRYPTO]
180
+ elif securityType == SecurityType.FOREX:
181
+ return [StandardSymbolsList.ALL_FOREX]
182
+ else:
183
+ # For STOCK and other types, return only STOCK security type lists except STOCKS
184
+ return [symbol_list for symbol_list in StandardSymbolsList
185
+ if symbol_list.securityType == SecurityType.STOCK and symbol_list != StandardSymbolsList.STOCKS]
186
+
145
187
  @staticmethod
146
188
  def fromDict(json_dict: Dict[str, Any]) -> SecurityUniverseSelector:
147
- scopeType = SecurityUniverseType[json_dict['universeType']]
148
- standardList = StandardSymbolsList[json_dict['standardList']] if 'standardList' in json_dict else None
189
+ securityType = SecurityType[json_dict['securityType']]
190
+ universeType = SecurityUniverseType[json_dict['universeType']]
191
+ standardList = None
192
+ if 'standardList' in json_dict:
193
+ list_name = json_dict['standardList']
194
+ standardList = StandardSymbolsList(list_name)
149
195
  customList = CustomSecurityList.fromJson(json_dict['customList']) if 'customList' in json_dict else None
150
196
  fundamentalQuery = FinancialQuery.fromDict(json_dict['financialQuery']) if 'financialQuery' in json_dict else None
151
- return SecurityUniverseSelector(scopeType, standardList, customList, cast(FinancialQuery, fundamentalQuery))
197
+
198
+ return SecurityUniverseSelector(securityType, universeType, standardList, customList, cast(FinancialQuery, fundamentalQuery))
152
199
 
153
200
  def toDict(self) -> Dict[str, Any]:
154
- jsonDict: Dict[str, Any] = {'universeType': self.universeType.value}
201
+ jsonDict: Dict[str, Any] = {
202
+ 'securityType': self.securityType.value,
203
+ 'universeType': self.universeType.value
204
+ }
155
205
  if self.standardList is not None:
156
206
  jsonDict["standardList"] = self.standardList.value
157
207
  if self.customList is not None:
@@ -163,48 +213,74 @@ class SecurityUniverseSelector:
163
213
  @staticmethod
164
214
  def singleStock(symbol: str) -> SecurityUniverseSelector:
165
215
  scopeType = SecurityUniverseType.CUSTOM_LIST
166
- customList = CustomSecurityList(SecurityType.STOCK)
216
+ customList = CustomSecurityList()
167
217
  customList.addSymbol(symbol)
168
- return SecurityUniverseSelector(scopeType, customList=customList)
218
+ return SecurityUniverseSelector(SecurityType.STOCK, scopeType, customList=customList)
169
219
 
170
220
  @staticmethod
171
- def fromStockSymbols(symbols: List[str]) -> SecurityUniverseSelector:
221
+ def fromSecurity(security: Security) -> SecurityUniverseSelector:
172
222
  scopeType = SecurityUniverseType.CUSTOM_LIST
173
- customList = CustomSecurityList(SecurityType.STOCK)
223
+ customList = CustomSecurityList()
224
+ customList.addSymbol(security.symbol)
225
+ return SecurityUniverseSelector(security.securityType, scopeType, customList=customList)
226
+
227
+ @staticmethod
228
+ def fromSymbols(securityType: SecurityType, symbols: List[str]) -> SecurityUniverseSelector:
229
+ scopeType = SecurityUniverseType.CUSTOM_LIST
230
+ customList = CustomSecurityList()
174
231
  customList.symbols = symbols
175
- return SecurityUniverseSelector(scopeType, customList=customList)
232
+ return SecurityUniverseSelector(SecurityType.STOCK, scopeType, customList=customList)
176
233
 
177
234
  @staticmethod
178
235
  def fromStandardList(standardListName: StandardSymbolsList) -> SecurityUniverseSelector:
179
- universeType = SecurityUniverseType.STANDARD_LIST
180
- return SecurityUniverseSelector(universeType, standardList=standardListName)
236
+ return SecurityUniverseSelector(standardListName.securityType, SecurityUniverseType.STANDARD_LIST, standardList=standardListName)
181
237
 
182
238
  @staticmethod
183
239
  def fromFinancialQuery(financialQuery: FinancialQuery) -> SecurityUniverseSelector:
184
240
  universeType = SecurityUniverseType.FUNDAMENTAL_QUERY
185
- return SecurityUniverseSelector(universeType, financialQuery=financialQuery)
241
+ return SecurityUniverseSelector(SecurityType.STOCK, universeType, financialQuery=financialQuery)
186
242
 
187
- def getSecurityType(self) -> SecurityType:
188
- if self.universeType == SecurityUniverseType.STANDARD_LIST:
189
- return SecurityType.ETF if self.standardList == StandardSymbolsList.ETFS else SecurityType.STOCK
190
- elif self.universeType == SecurityUniverseType.CUSTOM_LIST:
191
- return cast(CustomSecurityList, self.customList).securityType
192
- else:
193
- return SecurityType.STOCK
194
243
 
195
244
  def validate(self) -> None:
245
+ # Note - Python should have exact code to validate SecurityUniverseSelector because custom strategy also return SecurityUniverseSelector
246
+ # Technically, we could avoid duplicate validation here, but we keep it here to avoid sending network call to Python server to fail fast
247
+
248
+ # Validate securityType is not null
249
+ if self.securityType is None:
250
+ raise Exception("SecurityUniverseSelector.securityType is required")
251
+
252
+ # Validate securityType is one of STOCK, CRYPTO, or FOREX
253
+ if self.securityType not in (SecurityType.STOCK, SecurityType.CRYPTO, SecurityType.FOREX):
254
+ raise Exception(f"SecurityUniverseSelector.securityType must be one of STOCK, CRYPTO, or FOREX, not {self.securityType}")
255
+
256
+ # Validate universeType is not null
196
257
  if self.universeType is None:
197
258
  raise Exception("SecurityUniverseSelector.universeType is required")
259
+
260
+ # For CRYPTO, ETF, CURRENCY - only STANDARD_LIST and CUSTOM_LIST are valid
261
+ if self.securityType != SecurityType.STOCK and self.universeType == SecurityUniverseType.FUNDAMENTAL_QUERY:
262
+ raise Exception(f"FUNDAMENTAL_QUERY universe type is only valid for SecurityType.STOCK, not for {self.securityType}")
263
+
198
264
  if self.universeType == SecurityUniverseType.STANDARD_LIST:
199
265
  if self.standardList is None:
200
266
  raise Exception("SecurityUniverseSelector.standardList is required for StandardList UniverseType")
267
+
268
+ # If using STANDARD_LIST, StandardSymbolsList.security_type must match securityType
269
+ if self.standardList.securityType != self.securityType:
270
+ raise Exception(f"StandardSymbolsList security type ({self.standardList.securityType}) must match SecurityUniverseSelector security type ({self.securityType})")
271
+
272
+ # Validate that the standardList is allowed for the given securityType
273
+ validLists = self.getValidSymbolLists(self.securityType)
274
+ if self.standardList not in validLists:
275
+ raise Exception(f"StandardSymbolsList ({self.standardList}) is not valid for SecurityType ({self.securityType}). Valid lists are: {validLists}")
276
+
201
277
  elif self.universeType == SecurityUniverseType.CUSTOM_LIST:
202
278
  if self.customList is None:
203
279
  raise Exception("SecurityUniverseSelector.customList is required for CustomList UniverseType")
204
280
  self.customList.validate()
281
+
205
282
  elif self.universeType == SecurityUniverseType.FUNDAMENTAL_QUERY:
206
283
  if self.financialQuery is None:
207
- raise Exception(
208
- "SecurityUniverseSelector.fundamentalQuery is required for FUNDAMENTAL_QUERY UniverseType")
284
+ raise Exception("SecurityUniverseSelector.fundamentalQuery is required for FUNDAMENTAL_QUERY UniverseType")
209
285
  self.financialQuery.validate()
210
286
 
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import List, Any, Dict
3
3
 
4
- from investfly.models.MarketData import Security
4
+ from investfly.models.MarketData import SecurityType, Security
5
5
  from investfly.models.SecurityUniverseSelector import SecurityUniverseSelector
6
6
  from investfly.models.StrategyModels import TradeSignal, StandardCloseCriteria
7
7
  from investfly.models.PortfolioModels import TradeOrder, OpenPosition, Portfolio, PositionType
@@ -17,6 +17,10 @@ class TradingStrategy(ABC):
17
17
  self.state: Dict[str, int | float | bool] = {}
18
18
  """The persisted state of the strategy. """
19
19
 
20
+ def getSecurityType(self) -> SecurityType:
21
+ return self.getSecurityUniverseSelector().securityType
22
+
23
+
20
24
  @abstractmethod
21
25
  def getSecurityUniverseSelector(self) -> SecurityUniverseSelector:
22
26
  """
@@ -7,7 +7,7 @@ The main class for defining custom technical indicator is `investfly.models.Indi
7
7
  """
8
8
 
9
9
  from investfly.models.CommonModels import DatedValue, TimeUnit, TimeDelta, Session
10
- from investfly.models.Indicator import ParamType, IndicatorParamSpec, IndicatorValueType, IndicatorSpec, Indicator
10
+ from investfly.models.Indicator import ParamType, IndicatorParamSpec, IndicatorValueType, IndicatorSpec, Indicator, INDICATORS
11
11
  from investfly.models.MarketData import SecurityType, Security, Quote, BarInterval, Bar
12
12
  from investfly.models.MarketDataIds import QuoteField, FinancialField, StandardIndicatorId
13
13
  from investfly.models.PortfolioModels import PositionType, TradeType, Broker, TradeOrder, OrderStatus, PendingOrder, Balances, CompletedTrade, OpenPosition, ClosedPosition, Portfolio, PortfolioPerformance
@@ -15,4 +15,5 @@ from investfly.models.SecurityUniverseSelector import StandardSymbolsList, Custo
15
15
  from investfly.models.StrategyModels import DataParams, ScheduleInterval, Schedule, TradeSignal, StandardCloseCriteria, PortfolioSecurityAllocator, BacktestStatus
16
16
  from investfly.models.TradingStrategy import TradingStrategy
17
17
  from investfly.models.SecurityFilterModels import DataType, DataParam, DataSource
18
+ from investfly.models.ModelUtils import ModelUtils
18
19
 
@@ -0,0 +1,98 @@
1
+ from typing import Dict, Any, List
2
+
3
+ from investfly.models import *
4
+ from investfly.utils import PercentBasedPortfolioAllocator
5
+
6
+
7
+ class MacdTrendFollowingStrategy(TradingStrategy):
8
+ """
9
+ A trend following strategy based on the MACD (Moving Average Convergence Divergence) indicator.
10
+
11
+ This strategy:
12
+ 1. Uses the MACD indicator to identify trend momentum
13
+ 2. Generates buy signals when the MACD line crosses above the signal line (bullish momentum)
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 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 getSecurityUniverseSelector(self) -> SecurityUniverseSelector:
22
+ """
23
+ Select the universe of securities to trade.
24
+ This strategy uses the S&P 100 stocks.
25
+ """
26
+ return SecurityUniverseSelector.fromStandardList(StandardSymbolsList.SP_100)
27
+
28
+ @DataParams({
29
+ # MACD is typically calculated with 12, 26, and 9 as the standard parameters
30
+ # We request the MACD line and the signal line (MACDS)
31
+ "macd": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.MACD, "barinterval": BarInterval.ONE_DAY, "count": 2},
32
+ "macds": {"datatype": DataType.INDICATOR, "indicator": INDICATORS.MACDS, "barinterval": BarInterval.ONE_DAY, "count": 2},
33
+ # Request the latest daily bar to get the closing price for signal strength calculation
34
+ "daily_bar": {"datatype": DataType.BARS, "barinterval": BarInterval.ONE_DAY, "count": 1}
35
+ })
36
+ def evaluateOpenTradeCondition(self, security: Security, data: Dict[str, Any]) -> TradeSignal | None:
37
+ """
38
+ Generate a buy signal when the MACD line crosses above the signal line,
39
+ indicating increasing bullish momentum.
40
+
41
+ The signal strength is calculated based on the magnitude of the difference
42
+ between MACD and signal line relative to the stock's closing price from the latest daily bar.
43
+
44
+ This method is called at most once per day when a new daily bar is available.
45
+ """
46
+ macd = data["macd"]
47
+ macds = data["macds"]
48
+ daily_bar = data["daily_bar"][-1] # Get the latest daily bar
49
+
50
+ # Check for MACD line crossing above the signal line (bullish crossover)
51
+ if macd[-1].value > macds[-1].value and macd[-2].value <= macds[-2].value:
52
+ # Get the closing price from the latest daily bar
53
+ closing_price = daily_bar.close
54
+
55
+ # Calculate signal strength as a percentage of the difference relative to price
56
+ # This helps prioritize stronger signals when allocating portfolio
57
+ macd_diff = abs(macd[-1].value - macds[-1].value)
58
+
59
+ # Calculate signal strength based on:
60
+ # 1. The magnitude of the MACD crossover (larger difference = stronger signal)
61
+ # 2. The relative size of the difference compared to the stock price
62
+ signal_strength = (macd_diff / closing_price) * 100
63
+
64
+ # Add a component based on the rate of change of MACD to further strengthen the signal
65
+ macd_change_rate = abs(macd[-1].value - macd[-2].value) / abs(macd[-2].value) if macd[-2].value != 0 else 0
66
+ signal_strength = signal_strength * (1 + macd_change_rate)
67
+
68
+ # Return a long position signal with the calculated strength
69
+ return TradeSignal(security, PositionType.LONG, signal_strength)
70
+
71
+ return None
72
+
73
+ def getStandardCloseCondition(self) -> StandardCloseCriteria:
74
+ """
75
+ Define standard exit criteria for positions:
76
+ - Take profit at 5% gain
77
+ - Stop loss at 3% loss
78
+ - Time-based exit after 10 trading days (to prevent holding positions too long)
79
+ """
80
+ return StandardCloseCriteria(
81
+ targetProfit=5, # Take profit at 5% gain
82
+ stopLoss=-3, # Stop loss at 3% loss
83
+ trailingStop=None, # No trailing stop for this strategy
84
+ timeOut=TimeDelta(10, TimeUnit.DAYS) # Exit after 10 trading days
85
+ )
86
+
87
+ def processOpenTradeSignals(self, portfolio: Portfolio, tradeSignals: List[TradeSignal]) -> List[TradeOrder]:
88
+ """
89
+ Process the trade signals and allocate the portfolio.
90
+ This strategy allocates to the top 5 stocks with the strongest signals.
91
+ """
92
+ # Sort trade signals by strength in descending order
93
+ sorted_signals = sorted(tradeSignals, key=lambda signal: signal.strength if signal.strength is not None else 0, reverse=True)
94
+
95
+ # Use the PercentBasedPortfolioAllocator to allocate the portfolio
96
+ # Allocate to the top 5 stocks with equal weight (20% each)
97
+ portfolioAllocator = PercentBasedPortfolioAllocator(5)
98
+ return portfolioAllocator.allocatePortfolio(portfolio, sorted_signals)