investfly-sdk 1.6__tar.gz → 1.8__tar.gz

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.
Files changed (51) hide show
  1. {investfly_sdk-1.6 → investfly_sdk-1.8}/PKG-INFO +16 -28
  2. {investfly_sdk-1.6 → investfly_sdk-1.8}/README.md +13 -2
  3. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/Indicator.py +73 -1
  4. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/MarketData.py +25 -19
  5. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/ModelUtils.py +9 -6
  6. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/PortfolioModels.py +8 -6
  7. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/SecurityUniverseSelector.py +103 -27
  8. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/TradingStrategy.py +5 -1
  9. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/__init__.py +2 -1
  10. investfly_sdk-1.8/investfly/samples/strategies/MacdTrendFollowingStrategy.py +98 -0
  11. investfly_sdk-1.8/investfly/samples/strategies/RsiMeanReversionStrategy.py +163 -0
  12. investfly_sdk-1.8/investfly/samples/strategies/SmaCrossOverTemplate.py +347 -0
  13. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/utils/CommonUtils.py +22 -15
  14. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/PKG-INFO +16 -28
  15. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/SOURCES.txt +4 -1
  16. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/requires.txt +0 -2
  17. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/top_level.txt +1 -0
  18. {investfly_sdk-1.6 → investfly_sdk-1.8}/pyproject.toml +9 -7
  19. investfly_sdk-1.8/talib/__init__.pyi +185 -0
  20. investfly_sdk-1.6/investfly/samples/strategies/SmaCrossOverTemplate.py +0 -133
  21. {investfly_sdk-1.6 → investfly_sdk-1.8}/LICENSE.txt +0 -0
  22. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/__init__.py +0 -0
  23. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/IndicatorApiClient.py +0 -0
  24. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/InvestflyApiClient.py +0 -0
  25. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/MarketDataApiClient.py +0 -0
  26. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/PortfolioApiClient.py +0 -0
  27. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/RestApiClient.py +0 -0
  28. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/StrategyApiClient.py +0 -0
  29. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/__init__.py +0 -0
  30. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/py.typed +0 -0
  31. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/cli/InvestflyCli.py +0 -0
  32. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/cli/__init__.py +0 -0
  33. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/CommonModels.py +0 -0
  34. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/MarketDataIds.py +0 -0
  35. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/SecurityFilterModels.py +0 -0
  36. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/StrategyModels.py +0 -0
  37. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/py.typed +0 -0
  38. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/__init__.py +0 -0
  39. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/IndicatorTemplate.py +0 -0
  40. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/NewsSentiment.py +0 -0
  41. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/RsiOfSma.py +0 -0
  42. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/SmaEmaAverage.py +0 -0
  43. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/__init__.py +0 -0
  44. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/strategies/SmaCrossOverStrategy.py +0 -0
  45. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/strategies/__init__.py +0 -0
  46. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/utils/PercentBasedPortfolioAllocator.py +0 -0
  47. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/utils/__init__.py +0 -0
  48. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/utils/py.typed +0 -0
  49. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/dependency_links.txt +0 -0
  50. {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/entry_points.txt +0 -0
  51. {investfly_sdk-1.6 → investfly_sdk-1.8}/setup.cfg +0 -0
@@ -1,32 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: investfly-sdk
3
- Version: 1.6
3
+ Version: 1.8
4
4
  Summary: Investfly SDK
5
5
  Author-email: "Investfly.com" <admin@investfly.com>
6
- License: The MIT License (MIT)
7
-
8
- Copyright (c) 2023+ Investfly, Finverse LLC
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
6
+ License: MIT
27
7
  Project-URL: Homepage, https://www.investfly.com
28
8
  Classifier: Programming Language :: Python :: 3
29
- Classifier: License :: OSI Approved :: MIT License
30
9
  Classifier: Operating System :: OS Independent
31
10
  Requires-Python: >=3.7
32
11
  Description-Content-Type: text/markdown
@@ -36,7 +15,6 @@ Requires-Dist: charset-normalizer==3.2.0
36
15
  Requires-Dist: idna==3.4
37
16
  Requires-Dist: pandas==2.0.3
38
17
  Requires-Dist: pandas-stubs==2.0.3.230814
39
- Requires-Dist: pandas-ta==0.3.14b0
40
18
  Requires-Dist: python-dateutil==2.8.2
41
19
  Requires-Dist: pytz==2023.3
42
20
  Requires-Dist: requests==2.31.0
@@ -45,7 +23,6 @@ Requires-Dist: six==1.16.0
45
23
  Requires-Dist: tzdata==2023.3
46
24
  Requires-Dist: urllib3==1.26.15
47
25
  Requires-Dist: numpy==1.26.4
48
- Requires-Dist: TA-Lib==0.4.28
49
26
  Requires-Dist: mypy==1.12.1
50
27
 
51
28
  # About
@@ -183,13 +160,24 @@ Using IDE editor will assist with auto-completion and type hints. Additionally,
183
160
  When using the IDE, open `investfly` directory created above as a project with your IDE.
184
161
  Make sure that Python Interpreter is configured to the virtual environment `investfly/venv/bin/python` created above.
185
162
 
163
+ ### TA-Lib Stubs
164
+ TA-Lib is a technical analysis library https://github.com/TA-Lib/ta-lib-python used to compute technical indicators by Investfly.
165
+ This library can also be used in custom indicators and strategies. However, installing Python ta-lib wrapper requires installing native ta-lib, which is challenging based on the OS you are working with.
166
+ So investfly-sdk ships with ta-lib stubs, so when you install investfly-sdk, pip does not try to install ta-lib.
167
+ This means that you can develop your code, but cannot run them locally if you are using ta-lib in your code. This is generally OK, because you will use the CLI
168
+ to upload your code to Investfly server, where it will be run.
169
+ If you want also want to run your code locally to test it, then follow the installation method described in the link above and then install ta-lib with the following command
170
+ ```commandline
171
+ pip install ta-lib==0.4.28
172
+ ```
173
+
186
174
 
187
175
  # API Docs
188
176
 
189
- API Docs are published at https://www.investfly.com/guides/docs/index.html
177
+ API Docs are published at https://www.investfly.com/apidocs/investfly.html
190
178
 
191
179
  # Getting Help
192
- Please email [admin@investfly.com](admin@investfly.com) for any support or bug report
180
+ Please email support@investfly.com for any support or bug report
193
181
 
194
182
 
195
183
 
@@ -133,13 +133,24 @@ Using IDE editor will assist with auto-completion and type hints. Additionally,
133
133
  When using the IDE, open `investfly` directory created above as a project with your IDE.
134
134
  Make sure that Python Interpreter is configured to the virtual environment `investfly/venv/bin/python` created above.
135
135
 
136
+ ### TA-Lib Stubs
137
+ TA-Lib is a technical analysis library https://github.com/TA-Lib/ta-lib-python used to compute technical indicators by Investfly.
138
+ This library can also be used in custom indicators and strategies. However, installing Python ta-lib wrapper requires installing native ta-lib, which is challenging based on the OS you are working with.
139
+ So investfly-sdk ships with ta-lib stubs, so when you install investfly-sdk, pip does not try to install ta-lib.
140
+ This means that you can develop your code, but cannot run them locally if you are using ta-lib in your code. This is generally OK, because you will use the CLI
141
+ to upload your code to Investfly server, where it will be run.
142
+ If you want also want to run your code locally to test it, then follow the installation method described in the link above and then install ta-lib with the following command
143
+ ```commandline
144
+ pip install ta-lib==0.4.28
145
+ ```
146
+
136
147
 
137
148
  # API Docs
138
149
 
139
- API Docs are published at https://www.investfly.com/guides/docs/index.html
150
+ API Docs are published at https://www.investfly.com/apidocs/investfly.html
140
151
 
141
152
  # Getting Help
142
- Please email [admin@investfly.com](admin@investfly.com) for any support or bug report
153
+ Please email support@investfly.com for any support or bug report
143
154
 
144
155
 
145
156
 
@@ -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