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.
- {investfly_sdk-1.6 → investfly_sdk-1.8}/PKG-INFO +16 -28
- {investfly_sdk-1.6 → investfly_sdk-1.8}/README.md +13 -2
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/Indicator.py +73 -1
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/MarketData.py +25 -19
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/ModelUtils.py +9 -6
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/PortfolioModels.py +8 -6
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/SecurityUniverseSelector.py +103 -27
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/TradingStrategy.py +5 -1
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/__init__.py +2 -1
- investfly_sdk-1.8/investfly/samples/strategies/MacdTrendFollowingStrategy.py +98 -0
- investfly_sdk-1.8/investfly/samples/strategies/RsiMeanReversionStrategy.py +163 -0
- investfly_sdk-1.8/investfly/samples/strategies/SmaCrossOverTemplate.py +347 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/utils/CommonUtils.py +22 -15
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/PKG-INFO +16 -28
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/SOURCES.txt +4 -1
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/requires.txt +0 -2
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/top_level.txt +1 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/pyproject.toml +9 -7
- investfly_sdk-1.8/talib/__init__.pyi +185 -0
- investfly_sdk-1.6/investfly/samples/strategies/SmaCrossOverTemplate.py +0 -133
- {investfly_sdk-1.6 → investfly_sdk-1.8}/LICENSE.txt +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/__init__.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/IndicatorApiClient.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/InvestflyApiClient.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/MarketDataApiClient.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/PortfolioApiClient.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/RestApiClient.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/StrategyApiClient.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/__init__.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/api/py.typed +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/cli/InvestflyCli.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/cli/__init__.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/CommonModels.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/MarketDataIds.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/SecurityFilterModels.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/StrategyModels.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/models/py.typed +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/__init__.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/IndicatorTemplate.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/NewsSentiment.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/RsiOfSma.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/SmaEmaAverage.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/indicators/__init__.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/strategies/SmaCrossOverStrategy.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/samples/strategies/__init__.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/utils/PercentBasedPortfolioAllocator.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/utils/__init__.py +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly/utils/py.typed +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/dependency_links.txt +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/investfly_sdk.egg-info/entry_points.txt +0 -0
- {investfly_sdk-1.6 → investfly_sdk-1.8}/setup.cfg +0 -0
@@ -1,32 +1,11 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: investfly-sdk
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.8
|
4
4
|
Summary: Investfly SDK
|
5
5
|
Author-email: "Investfly.com" <admin@investfly.com>
|
6
|
-
License:
|
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/
|
177
|
+
API Docs are published at https://www.investfly.com/apidocs/investfly.html
|
190
178
|
|
191
179
|
# Getting Help
|
192
|
-
Please email
|
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/
|
150
|
+
API Docs are published at https://www.investfly.com/apidocs/investfly.html
|
140
151
|
|
141
152
|
# Getting Help
|
142
|
-
Please email
|
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,
|
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
|
-
|
70
|
+
todayChangePct: float | None = None
|
69
71
|
dayOpen: float | None = None
|
70
72
|
dayHigh: float | None = None
|
71
73
|
dayLow: float | None = None
|
72
|
-
|
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.
|
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.
|
117
|
+
if self.todayChangePct is None:
|
115
118
|
raise Exception("DAY_CHANGE_PCT value not available in Quote")
|
116
|
-
return DatedValue(self.date, self.
|
119
|
+
return DatedValue(self.date, self.todayChangePct)
|
117
120
|
elif quoteField == QuoteField.DAY_VOLUME:
|
118
|
-
if self.
|
121
|
+
if self.volume is None:
|
119
122
|
raise Exception("DAY_VOLUME not available in Quote")
|
120
|
-
return DatedValue(self.date, self.
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
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
|
-
|
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.
|
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.
|
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.
|
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
|
-
|
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:
|
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:
|
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:
|
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:
|
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
|
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(
|
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
|
-
|
148
|
-
|
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
|
-
|
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] = {
|
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(
|
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
|
221
|
+
def fromSecurity(security: Security) -> SecurityUniverseSelector:
|
172
222
|
scopeType = SecurityUniverseType.CUSTOM_LIST
|
173
|
-
customList = CustomSecurityList(
|
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
|
-
|
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
|
|