investfly-sdk 1.6__py3-none-any.whl → 1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- investfly/models/Indicator.py +73 -1
- investfly/models/MarketData.py +25 -19
- investfly/models/ModelUtils.py +9 -6
- investfly/models/PortfolioModels.py +8 -6
- investfly/models/SecurityUniverseSelector.py +103 -27
- investfly/models/TradingStrategy.py +5 -1
- investfly/models/__init__.py +2 -1
- investfly/samples/strategies/MacdTrendFollowingStrategy.py +98 -0
- investfly/samples/strategies/RsiMeanReversionStrategy.py +163 -0
- investfly/samples/strategies/SmaCrossOverTemplate.py +284 -70
- investfly/utils/CommonUtils.py +22 -15
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/METADATA +30 -42
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/RECORD +18 -15
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/WHEEL +1 -1
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/top_level.txt +1 -0
- talib/__init__.pyi +185 -0
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/LICENSE.txt +0 -0
- {investfly_sdk-1.6.dist-info → investfly_sdk-1.8.dist-info}/entry_points.txt +0 -0
investfly/models/Indicator.py
CHANGED
@@ -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()
|
investfly/models/MarketData.py
CHANGED
@@ -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:
|
investfly/models/ModelUtils.py
CHANGED
@@ -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
|
"""
|
investfly/models/__init__.py
CHANGED
@@ -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)
|