investfly-sdk 1.0__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/__init__.py +0 -0
- investfly/api/InvestflyApiClient.py +23 -0
- investfly/api/MarketDataApiClient.py +16 -0
- investfly/api/PortfolioApiClient.py +37 -0
- investfly/api/RestApiClient.py +81 -0
- investfly/api/__init__.py +0 -0
- investfly/cli/CliApiClient.py +46 -0
- investfly/cli/__init__.py +0 -0
- investfly/cli/commands.py +78 -0
- investfly/models/CommonModels.py +70 -0
- investfly/models/Indicator.py +164 -0
- investfly/models/MarketData.py +177 -0
- investfly/models/MarketDataIds.py +119 -0
- investfly/models/ModelUtils.py +34 -0
- investfly/models/PortfolioModels.py +270 -0
- investfly/models/SecurityFilterModels.py +167 -0
- investfly/models/SecurityUniverseSelector.py +202 -0
- investfly/models/StrategyModels.py +124 -0
- investfly/models/TradingStrategy.py +59 -0
- investfly/models/__init__.py +10 -0
- investfly/samples/__init__.py +0 -0
- investfly/samples/indicators/IndicatorTemplate.py +80 -0
- investfly/samples/indicators/NewsSentiment.py +41 -0
- investfly/samples/indicators/RsiOfSma.py +42 -0
- investfly/samples/indicators/SmaEmaAverage.py +40 -0
- investfly/samples/indicators/__init__.py +0 -0
- investfly/samples/strategies/SmaCrossOverStrategy.py +35 -0
- investfly/samples/strategies/SmaCrossOverTemplate.py +131 -0
- investfly/samples/strategies/__init__.py +0 -0
- investfly/utils/CommonUtils.py +79 -0
- investfly/utils/PercentBasedPortfolioAllocator.py +35 -0
- investfly/utils/__init__.py +2 -0
- investfly_sdk-1.0.dist-info/LICENSE.txt +21 -0
- investfly_sdk-1.0.dist-info/METADATA +53 -0
- investfly_sdk-1.0.dist-info/RECORD +38 -0
- investfly_sdk-1.0.dist-info/WHEEL +5 -0
- investfly_sdk-1.0.dist-info/entry_points.txt +2 -0
- investfly_sdk-1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,167 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import numbers
|
4
|
+
from enum import Enum
|
5
|
+
from typing import Dict, Any, cast
|
6
|
+
|
7
|
+
from investfly.models.MarketData import BarInterval
|
8
|
+
from investfly.models.MarketDataIds import QuoteField, FinancialField
|
9
|
+
|
10
|
+
|
11
|
+
class DataSource(str, Enum):
|
12
|
+
BARS = "BARS"
|
13
|
+
FINANCIAL = "FINANCIAL"
|
14
|
+
QUOTE = "QUOTE"
|
15
|
+
NEWS = "NEWS"
|
16
|
+
|
17
|
+
|
18
|
+
class DataType(str, Enum):
|
19
|
+
BARS = "BARS"
|
20
|
+
FINANCIAL = "FINANCIAL"
|
21
|
+
QUOTE = "QUOTE"
|
22
|
+
NEWS = "NEWS"
|
23
|
+
|
24
|
+
INDICATOR = "INDICATOR"
|
25
|
+
CONST = "CONST"
|
26
|
+
|
27
|
+
def __str__(self):
|
28
|
+
return self.value
|
29
|
+
|
30
|
+
def __repr__(self):
|
31
|
+
return self.value
|
32
|
+
|
33
|
+
|
34
|
+
class ConstUnit(str, Enum):
|
35
|
+
K = "K"
|
36
|
+
M = "M"
|
37
|
+
B = "B"
|
38
|
+
|
39
|
+
|
40
|
+
class DataParam(Dict[str, Any]):
|
41
|
+
SECURITY = "security"
|
42
|
+
FIELD = "field"
|
43
|
+
INDICATOR = "indicator"
|
44
|
+
DATATYPE = "datatype"
|
45
|
+
VALUE = "value"
|
46
|
+
UNIT = "unit"
|
47
|
+
BARINTERVAL = "barinterval"
|
48
|
+
LOOKBACK = "lookback"
|
49
|
+
COUNT = "count"
|
50
|
+
|
51
|
+
def setDataType(self, dataType: DataType) -> None:
|
52
|
+
self[DataParam.DATATYPE] = dataType
|
53
|
+
|
54
|
+
def getDataType(self) -> DataType:
|
55
|
+
return cast(DataType, self.get(DataParam.DATATYPE))
|
56
|
+
|
57
|
+
def getIndicatorId(self) -> str:
|
58
|
+
return cast(str, self.get(DataParam.INDICATOR))
|
59
|
+
|
60
|
+
def getBarInterval(self) -> BarInterval|None:
|
61
|
+
return self.get(DataParam.BARINTERVAL)
|
62
|
+
|
63
|
+
def getQuoteField(self) -> QuoteField | None:
|
64
|
+
return self.get(DataParam.FIELD)
|
65
|
+
|
66
|
+
def getFinancialField(self) -> FinancialField | None:
|
67
|
+
return self.get(DataParam.FIELD)
|
68
|
+
|
69
|
+
def getCount(self) -> int | None:
|
70
|
+
return self.get(DataParam.COUNT)
|
71
|
+
|
72
|
+
def getLookback(self) -> int | None:
|
73
|
+
return self.get(DataParam.LOOKBACK)
|
74
|
+
|
75
|
+
def getConstValue(self) -> int | float:
|
76
|
+
val = cast(int|float, self.get(DataParam.VALUE))
|
77
|
+
unit: ConstUnit|None = self.get(DataParam.UNIT)
|
78
|
+
if unit is None:
|
79
|
+
return val
|
80
|
+
elif unit == ConstUnit.K:
|
81
|
+
return val * 1000
|
82
|
+
elif unit == ConstUnit.M:
|
83
|
+
return val * 1000000
|
84
|
+
elif unit == ConstUnit.B:
|
85
|
+
return val * 1000000000
|
86
|
+
else:
|
87
|
+
raise Exception(f"Unknown unit {unit}")
|
88
|
+
|
89
|
+
def getSecurity(self) -> str | None:
|
90
|
+
return self.get(DataParam.SECURITY)
|
91
|
+
|
92
|
+
def validate(self) -> None:
|
93
|
+
dataType: DataType|None = self.get(DataParam.DATATYPE)
|
94
|
+
if dataType is None:
|
95
|
+
raise Exception("'datatype' attribute is required for all data parameters")
|
96
|
+
if not isinstance(dataType, DataType):
|
97
|
+
raise Exception(f"'datatype' must of of type Enum DataType")
|
98
|
+
|
99
|
+
if dataType == DataType.CONST:
|
100
|
+
value: numbers.Number|None = self.get(DataParam.VALUE)
|
101
|
+
unit: ConstUnit|None = self.get(DataParam.UNIT)
|
102
|
+
if value is None:
|
103
|
+
raise Exception("'value' attribute is required for CONST datatype")
|
104
|
+
if not isinstance(value, numbers.Number):
|
105
|
+
raise Exception("const value must be a number")
|
106
|
+
if unit is not None and not isinstance(unit, ConstUnit):
|
107
|
+
raise Exception("const unit must be of type ConstUnit")
|
108
|
+
|
109
|
+
elif dataType == DataType.QUOTE:
|
110
|
+
quoteField: QuoteField|None = self.get(DataParam.FIELD)
|
111
|
+
if quoteField is not None:
|
112
|
+
if not isinstance(quoteField, QuoteField):
|
113
|
+
raise Exception("'field' attribute for 'Quote' datatype must be QuoteField")
|
114
|
+
|
115
|
+
elif dataType == DataType.FINANCIAL:
|
116
|
+
financialField: FinancialField|None = self.get(DataParam.FIELD)
|
117
|
+
if financialField is not None:
|
118
|
+
if not isinstance(financialField, FinancialField):
|
119
|
+
raise Exception("'field' attribute for 'Quote' datatype must be FinancialField")
|
120
|
+
|
121
|
+
elif dataType == DataType.INDICATOR:
|
122
|
+
indicatorId: str|None = self.getIndicatorId()
|
123
|
+
if indicatorId is None:
|
124
|
+
raise Exception("'indicator' attribute is required for Indicator datatype")
|
125
|
+
|
126
|
+
elif dataType == DataType.BARS:
|
127
|
+
barPrice = self.get("price")
|
128
|
+
if barPrice is not None:
|
129
|
+
if barPrice not in ["open", "high", "low", "close", "volume"]:
|
130
|
+
raise Exception("'price' attribute in BARS type must be on of [open, high, low, close, volume]")
|
131
|
+
|
132
|
+
@staticmethod
|
133
|
+
def fromDict(json_dict: Dict[str, Any]) -> DataParam:
|
134
|
+
dataParam = DataParam()
|
135
|
+
dataType: DataType = DataType[cast(str, json_dict.get(DataParam.DATATYPE))]
|
136
|
+
dataParam.setDataType(dataType)
|
137
|
+
|
138
|
+
for key in json_dict.keys():
|
139
|
+
value = json_dict[key]
|
140
|
+
if key == DataParam.DATATYPE:
|
141
|
+
continue
|
142
|
+
elif key == DataParam.BARINTERVAL:
|
143
|
+
dataParam[key] = BarInterval[value]
|
144
|
+
elif key == DataParam.FIELD:
|
145
|
+
if dataType == DataType.QUOTE:
|
146
|
+
dataParam[key] = QuoteField[value]
|
147
|
+
elif dataType == DataType.FINANCIAL:
|
148
|
+
dataParam[key] = FinancialField[value]
|
149
|
+
else:
|
150
|
+
dataParam[key] = value
|
151
|
+
elif key == DataParam.INDICATOR:
|
152
|
+
# Since indicator can also be custom indicator, it cant be converted to StandardIndicatorId enum
|
153
|
+
dataParam[key] = value
|
154
|
+
elif key == DataParam.VALUE:
|
155
|
+
# json.loads should already have converted to int or float based on the value
|
156
|
+
dataParam[key] = value
|
157
|
+
elif key == DataParam.UNIT:
|
158
|
+
dataParam[key] = ConstUnit[value]
|
159
|
+
else:
|
160
|
+
dataParam[key] = value
|
161
|
+
return dataParam
|
162
|
+
|
163
|
+
def clone(self) -> DataParam:
|
164
|
+
dataParam: DataParam = DataParam()
|
165
|
+
for key in self.keys():
|
166
|
+
dataParam[key] = self.get(key)
|
167
|
+
return dataParam
|
@@ -0,0 +1,202 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from enum import Enum
|
5
|
+
from numbers import Number
|
6
|
+
from typing import List, Dict, Any, cast
|
7
|
+
|
8
|
+
from investfly.models.MarketData import SecurityType
|
9
|
+
from investfly.models.MarketDataIds import FinancialField
|
10
|
+
|
11
|
+
|
12
|
+
class StandardSymbolsList(str, Enum):
|
13
|
+
SP_100 = "SP_100"
|
14
|
+
SP_500 = "SP_500"
|
15
|
+
NASDAQ_100 = "NASDAQ_100"
|
16
|
+
NASDAQ_COMPOSITE = "NASDAQ_COMPOSITE"
|
17
|
+
RUSSELL_1000 = "RUSSELL_1000"
|
18
|
+
DOW_JONES_INDUSTRIALS = "DOW_JONES_INDUSTRIALS"
|
19
|
+
ETFS = "ETFS"
|
20
|
+
|
21
|
+
def __str__(self):
|
22
|
+
return self.value
|
23
|
+
|
24
|
+
def __repr__(self):
|
25
|
+
return self.value
|
26
|
+
|
27
|
+
|
28
|
+
class CustomSecurityList:
|
29
|
+
def __init__(self, securityType: SecurityType):
|
30
|
+
self.securityType = securityType
|
31
|
+
self.symbols: List[str] = []
|
32
|
+
|
33
|
+
def addSymbol(self, symbol: str) -> None:
|
34
|
+
self.symbols.append(symbol)
|
35
|
+
|
36
|
+
@staticmethod
|
37
|
+
def fromJson(json_dict: Dict[str, Any]) -> CustomSecurityList:
|
38
|
+
securityList = CustomSecurityList(SecurityType(json_dict['securityType']))
|
39
|
+
securityList.symbols = json_dict['symbols']
|
40
|
+
return securityList
|
41
|
+
|
42
|
+
def toDict(self) -> Dict[str, Any]:
|
43
|
+
return self.__dict__.copy()
|
44
|
+
|
45
|
+
def validate(self) -> None:
|
46
|
+
if self.securityType is None:
|
47
|
+
raise Exception("CustomSecurityList.securityType is required")
|
48
|
+
if len(self.symbols) == 0:
|
49
|
+
raise Exception("CustomSecurityList.symbols: At least one symbol is required")
|
50
|
+
|
51
|
+
|
52
|
+
class SecurityUniverseType(str, Enum):
|
53
|
+
STANDARD_LIST = "STANDARD_LIST",
|
54
|
+
CUSTOM_LIST = "CUSTOM_LIST",
|
55
|
+
FUNDAMENTAL_QUERY = "FUNDAMENTAL_QUERY"
|
56
|
+
|
57
|
+
|
58
|
+
class ComparisonOperator(str, Enum):
|
59
|
+
GREATER_THAN = ">"
|
60
|
+
LESS_THAN = "<"
|
61
|
+
GREATER_OR_EQUAL = ">="
|
62
|
+
LESS_OR_EQUAL = "<="
|
63
|
+
EQUAL_TO = "=="
|
64
|
+
|
65
|
+
|
66
|
+
@dataclass
|
67
|
+
class FinancialCondition:
|
68
|
+
financialField: FinancialField
|
69
|
+
operator: ComparisonOperator
|
70
|
+
value: str | FinancialField
|
71
|
+
|
72
|
+
@staticmethod
|
73
|
+
def fromDict(json_dict: Dict[str, Any]) -> FinancialCondition:
|
74
|
+
financialField = FinancialField[json_dict['financialField']]
|
75
|
+
operator = ComparisonOperator[json_dict['operator']]
|
76
|
+
valueFromJson = json_dict['value']
|
77
|
+
allFinancialFields = [cast(FinancialField, f).name for f in FinancialField]
|
78
|
+
if valueFromJson is allFinancialFields:
|
79
|
+
value = FinancialField[valueFromJson]
|
80
|
+
else:
|
81
|
+
value = valueFromJson
|
82
|
+
|
83
|
+
return FinancialCondition(financialField, operator, value)
|
84
|
+
|
85
|
+
def toDict(self) -> Dict[str, Any]:
|
86
|
+
return self.__dict__.copy()
|
87
|
+
|
88
|
+
def validate(self) -> None:
|
89
|
+
if not isinstance(self.financialField, FinancialField):
|
90
|
+
raise Exception("Left expression in financial query must be of type FinancialField")
|
91
|
+
if not isinstance(self.value, FinancialField) and not isinstance(self.value, str):
|
92
|
+
raise Exception("Right expression in financial query must of type Financial Field or string")
|
93
|
+
if isinstance(self.value, str):
|
94
|
+
# it must represent a number
|
95
|
+
valueStr = self.value
|
96
|
+
if valueStr.endswith("K") or valueStr.endswith("M") or valueStr.endswith("B"):
|
97
|
+
valueStr = valueStr[:-1]
|
98
|
+
if not valueStr.replace('.', '', 1).isdigit():
|
99
|
+
raise Exception(f"Right expression offFinancial query must be a number or Financial Field. You provided: {self.value}")
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
class FinancialQuery:
|
104
|
+
def __init__(self) -> None:
|
105
|
+
self.queryConditions: List[FinancialCondition] = []
|
106
|
+
|
107
|
+
def addCondition(self, condition: FinancialCondition) -> None:
|
108
|
+
self.queryConditions.append(condition)
|
109
|
+
|
110
|
+
@staticmethod
|
111
|
+
def fromDict(json_dict: Dict[str, Any]) -> FinancialQuery:
|
112
|
+
financialQuery = FinancialQuery()
|
113
|
+
conditionsList = json_dict['queryConditions']
|
114
|
+
for cond in conditionsList:
|
115
|
+
financialQuery.queryConditions.append(FinancialCondition.fromDict(cond))
|
116
|
+
return financialQuery
|
117
|
+
|
118
|
+
def toDict(self) -> Dict[str, Any]:
|
119
|
+
return {'queryConditions': [q.toDict() for q in self.queryConditions]}
|
120
|
+
|
121
|
+
def validate(self) -> None:
|
122
|
+
if len(self.queryConditions) == 0:
|
123
|
+
raise Exception("FinancialQuery must have at least one criteria")
|
124
|
+
for f in self.queryConditions:
|
125
|
+
f.validate()
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
@dataclass
|
130
|
+
class SecurityUniverseSelector:
|
131
|
+
universeType: SecurityUniverseType
|
132
|
+
standardList: StandardSymbolsList | None = None
|
133
|
+
customList: CustomSecurityList | None = None
|
134
|
+
|
135
|
+
financialQuery: FinancialQuery | None = None
|
136
|
+
|
137
|
+
@staticmethod
|
138
|
+
def fromDict(json_dict: Dict[str, Any]) -> SecurityUniverseSelector:
|
139
|
+
scopeType = SecurityUniverseType[json_dict['universeType']]
|
140
|
+
standardList = StandardSymbolsList[json_dict['standardList']] if 'standardList' in json_dict else None
|
141
|
+
customList = CustomSecurityList.fromJson(json_dict['customList']) if 'customList' in json_dict else None
|
142
|
+
fundamentalQuery = FinancialQuery.fromDict(json_dict['financialQuery']) if 'financialQuery' in json_dict else None
|
143
|
+
return SecurityUniverseSelector(scopeType, standardList, customList, cast(FinancialQuery, fundamentalQuery))
|
144
|
+
|
145
|
+
def toDict(self) -> Dict[str, Any]:
|
146
|
+
jsonDict: Dict[str, Any] = {'universeType': self.universeType.value}
|
147
|
+
if self.standardList is not None:
|
148
|
+
jsonDict["standardList"] = self.standardList.value
|
149
|
+
if self.customList is not None:
|
150
|
+
jsonDict['customList'] = self.customList.toDict()
|
151
|
+
if self.financialQuery is not None:
|
152
|
+
jsonDict["financialQuery"] = self.financialQuery.toDict()
|
153
|
+
return jsonDict
|
154
|
+
|
155
|
+
@staticmethod
|
156
|
+
def singleStock(symbol: str) -> SecurityUniverseSelector:
|
157
|
+
scopeType = SecurityUniverseType.CUSTOM_LIST
|
158
|
+
customList = CustomSecurityList(SecurityType.STOCK)
|
159
|
+
customList.addSymbol(symbol)
|
160
|
+
return SecurityUniverseSelector(scopeType, customList=customList)
|
161
|
+
|
162
|
+
@staticmethod
|
163
|
+
def fromStockSymbols(symbols: List[str]) -> SecurityUniverseSelector:
|
164
|
+
scopeType = SecurityUniverseType.CUSTOM_LIST
|
165
|
+
customList = CustomSecurityList(SecurityType.STOCK)
|
166
|
+
customList.symbols = symbols
|
167
|
+
return SecurityUniverseSelector(scopeType, customList=customList)
|
168
|
+
|
169
|
+
@staticmethod
|
170
|
+
def fromStandardList(standardListName: StandardSymbolsList) -> SecurityUniverseSelector:
|
171
|
+
universeType = SecurityUniverseType.STANDARD_LIST
|
172
|
+
return SecurityUniverseSelector(universeType, standardList=standardListName)
|
173
|
+
|
174
|
+
@staticmethod
|
175
|
+
def fromFinancialQuery(financialQuery: FinancialQuery) -> SecurityUniverseSelector:
|
176
|
+
universeType = SecurityUniverseType.FUNDAMENTAL_QUERY
|
177
|
+
return SecurityUniverseSelector(universeType, financialQuery=financialQuery)
|
178
|
+
|
179
|
+
def getSecurityType(self) -> SecurityType:
|
180
|
+
if self.universeType == SecurityUniverseType.STANDARD_LIST:
|
181
|
+
return SecurityType.ETF if self.standardList == StandardSymbolsList.ETFS else SecurityType.STOCK
|
182
|
+
elif self.universeType == SecurityUniverseType.CUSTOM_LIST:
|
183
|
+
return cast(CustomSecurityList, self.customList).securityType
|
184
|
+
else:
|
185
|
+
return SecurityType.STOCK
|
186
|
+
|
187
|
+
def validate(self) -> None:
|
188
|
+
if self.universeType is None:
|
189
|
+
raise Exception("SecurityUniverseSelector.universeType is required")
|
190
|
+
if self.universeType == SecurityUniverseType.STANDARD_LIST:
|
191
|
+
if self.standardList is None:
|
192
|
+
raise Exception("SecurityUniverseSelector.standardList is required for StandardList UniverseType")
|
193
|
+
elif self.universeType == SecurityUniverseType.CUSTOM_LIST:
|
194
|
+
if self.customList is None:
|
195
|
+
raise Exception("SecurityUniverseSelector.customList is required for CustomList UniverseType")
|
196
|
+
self.customList.validate()
|
197
|
+
elif self.universeType == SecurityUniverseType.FUNDAMENTAL_QUERY:
|
198
|
+
if self.financialQuery is None:
|
199
|
+
raise Exception(
|
200
|
+
"SecurityUniverseSelector.fundamentalQuery is required for FUNDAMENTAL_QUERY UniverseType")
|
201
|
+
self.financialQuery.validate()
|
202
|
+
|
@@ -0,0 +1,124 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from datetime import datetime
|
6
|
+
from enum import Enum
|
7
|
+
from typing import Dict, Any, List
|
8
|
+
|
9
|
+
|
10
|
+
from investfly.models.CommonModels import TimeDelta
|
11
|
+
from investfly.models.MarketData import Security
|
12
|
+
from investfly.models.ModelUtils import ModelUtils
|
13
|
+
from investfly.models.PortfolioModels import PositionType, Portfolio, TradeOrder
|
14
|
+
from investfly.models.SecurityFilterModels import DataParam
|
15
|
+
|
16
|
+
|
17
|
+
def DataParams(params: Dict[str, Dict[str, Any]]):
|
18
|
+
def decorator_func(func):
|
19
|
+
def wrapper_func(*args, **kwargs):
|
20
|
+
return func(*args, **kwargs)
|
21
|
+
|
22
|
+
dataParams = {}
|
23
|
+
for key in params.keys():
|
24
|
+
paramDict = params[key]
|
25
|
+
dataParam = DataParam.fromDict(paramDict)
|
26
|
+
dataParams[key] = dataParam
|
27
|
+
return wrapper_func, dataParams
|
28
|
+
|
29
|
+
return decorator_func
|
30
|
+
|
31
|
+
|
32
|
+
class ScheduleInterval(str, Enum):
|
33
|
+
DAILY_AFTER_MARKET_OPEN = "DAILY_AFTER_MARKET_OPEN"
|
34
|
+
DAILY_AFTER_MARKET_CLOSE = "DAILY_AFTER_MARKET_CLOSE"
|
35
|
+
HOURLY_DURING_MARKET_OPEN = "HOURLY_DURING_MARKET_OPEN"
|
36
|
+
|
37
|
+
def __str__(self):
|
38
|
+
return self.value
|
39
|
+
|
40
|
+
def __repr__(self):
|
41
|
+
return self.value
|
42
|
+
|
43
|
+
|
44
|
+
def Schedule(param: ScheduleInterval | None):
|
45
|
+
def decorator_func(func):
|
46
|
+
def wrapper_func(*args, **kwargs):
|
47
|
+
return func(*args, **kwargs)
|
48
|
+
|
49
|
+
return wrapper_func, param
|
50
|
+
|
51
|
+
return decorator_func
|
52
|
+
|
53
|
+
|
54
|
+
@dataclass
|
55
|
+
class TradeSignal:
|
56
|
+
security: Security
|
57
|
+
position: PositionType
|
58
|
+
strength: int = 1
|
59
|
+
data: Dict[str, Any] | None = None # Any other data besides strength that could be useful to generate TradeOrder
|
60
|
+
|
61
|
+
|
62
|
+
@dataclass
|
63
|
+
class StandardCloseCriteria:
|
64
|
+
targetProfit: float | None # specify in scaled [0 - 100 range]
|
65
|
+
stopLoss: float | None # specify as negative
|
66
|
+
trailingStop: float| None # specify as negative
|
67
|
+
timeOut: TimeDelta | None
|
68
|
+
|
69
|
+
@staticmethod
|
70
|
+
def fromDict(json_dict: Dict[str, Any]) -> StandardCloseCriteria:
|
71
|
+
return StandardCloseCriteria(
|
72
|
+
json_dict.get('targetProfit'),
|
73
|
+
json_dict.get('stopLoss'),
|
74
|
+
json_dict.get('trailingStop'),
|
75
|
+
TimeDelta.fromDict(json_dict['timeout']) if 'timeout' in json_dict else None
|
76
|
+
)
|
77
|
+
|
78
|
+
def toDict(self) -> Dict[str, Any]:
|
79
|
+
jsonDict = self.__dict__.copy()
|
80
|
+
if self.timeOut is not None:
|
81
|
+
jsonDict['timeout'] = self.timeOut.toDict()
|
82
|
+
return jsonDict
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
class PortfolioSecurityAllocator(ABC):
|
87
|
+
|
88
|
+
@abstractmethod
|
89
|
+
def allocatePortfolio(self, portfolio: Portfolio, tradeSignals: List[TradeSignal]) -> List[TradeOrder]:
|
90
|
+
pass
|
91
|
+
|
92
|
+
|
93
|
+
class LogLevel(str, Enum):
|
94
|
+
INFO = "INFO"
|
95
|
+
WARN = "WARN"
|
96
|
+
ERROR = "ERROR"
|
97
|
+
|
98
|
+
@dataclass
|
99
|
+
class DeploymentLog:
|
100
|
+
date: datetime
|
101
|
+
level: LogLevel
|
102
|
+
message: str
|
103
|
+
|
104
|
+
@staticmethod
|
105
|
+
def info(message: str) -> DeploymentLog:
|
106
|
+
return DeploymentLog(datetime.now(), LogLevel.INFO, message)
|
107
|
+
|
108
|
+
@staticmethod
|
109
|
+
def warn(message: str) -> DeploymentLog:
|
110
|
+
return DeploymentLog(datetime.now(), LogLevel.WARN, message)
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def error(message: str) -> DeploymentLog:
|
114
|
+
return DeploymentLog(datetime.now(), LogLevel.ERROR, message)
|
115
|
+
|
116
|
+
def toDict(self) -> Dict[str, Any]:
|
117
|
+
dict = self.__dict__.copy()
|
118
|
+
dict['date'] = ModelUtils.formatDatetime(self.date)
|
119
|
+
return dict
|
120
|
+
|
121
|
+
@staticmethod
|
122
|
+
def fromDict(json_dict: Dict[str, Any]) -> DeploymentLog:
|
123
|
+
return DeploymentLog(ModelUtils.parseDatetime(json_dict['date']), LogLevel(json_dict['level']), json_dict['message'])
|
124
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import List, Any, Dict
|
3
|
+
|
4
|
+
from investfly.models.MarketData import Security
|
5
|
+
from investfly.models.SecurityUniverseSelector import SecurityUniverseSelector
|
6
|
+
from investfly.models.StrategyModels import TradeSignal, StandardCloseCriteria
|
7
|
+
from investfly.models.PortfolioModels import TradeOrder, OpenPosition, Portfolio, PositionType
|
8
|
+
from investfly.utils.PercentBasedPortfolioAllocator import PercentBasedPortfolioAllocator
|
9
|
+
|
10
|
+
|
11
|
+
class TradingStrategy(ABC):
|
12
|
+
|
13
|
+
def __init__(self) -> None:
|
14
|
+
self.state: Dict[str, int | float | bool] = {}
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
def getSecurityUniverseSelector(self) -> SecurityUniverseSelector:
|
18
|
+
pass
|
19
|
+
|
20
|
+
"""
|
21
|
+
This function must be annotated with OnData to indicate when should this function be called.
|
22
|
+
The function is called whenever a new data is available based on the subscribed data
|
23
|
+
This function is called separately for each security
|
24
|
+
@DataParams({
|
25
|
+
"sma2": {"datatype": DataType.INDICATOR, "indicator": "SMA", "barinterval": BarInterval.ONE_MINUTE, "period": 2, "count": 2},
|
26
|
+
"sma3": {"datatype": DataType.INDICATOR, "indicator": "SMA", "barinterval": BarInterval.ONE_MINUTE, "period": 3, "count": 2},
|
27
|
+
"allOneMinBars": {"datatype": DataType.BARS, "barinterval": BarInterval.ONE_MINUTE},
|
28
|
+
"latestDailyBar": {"datatype": DataType.BARS, "barinterval": BarInterval.ONE_DAY, "count":1},
|
29
|
+
"quote": {"datatype": DataType.QUOTE},
|
30
|
+
"lastprice": {"datatype": DataType.QUOTE, "field": QuoteField.LASTPRICE},
|
31
|
+
"allFinancials": {"datatype": DataType.FINANCIAL},
|
32
|
+
"revenue": {"datatype": DataType.FINANCIAL, "field": FinancialField.REVENUE}
|
33
|
+
|
34
|
+
})
|
35
|
+
"""
|
36
|
+
@abstractmethod
|
37
|
+
def evaluateOpenTradeCondition(self, security: Security, data: Dict[str, Any]) -> TradeSignal | None:
|
38
|
+
pass
|
39
|
+
|
40
|
+
def processOpenTradeSignals(self, portfolio: Portfolio, tradeSignals: List[TradeSignal]) -> List[TradeOrder]:
|
41
|
+
portfolioAllocator = PercentBasedPortfolioAllocator(10, PositionType.LONG)
|
42
|
+
return portfolioAllocator.allocatePortfolio(portfolio, tradeSignals)
|
43
|
+
|
44
|
+
def getStandardCloseCondition(self) -> StandardCloseCriteria | None:
|
45
|
+
# Note that these are always executed as MARKET_ORDER
|
46
|
+
return None
|
47
|
+
|
48
|
+
def evaluateCloseTradeCondition(self, openPos: OpenPosition, data) -> TradeOrder | None:
|
49
|
+
return None
|
50
|
+
|
51
|
+
def runAtInterval(self, portfolio: Portfolio) -> List[TradeOrder]:
|
52
|
+
return []
|
53
|
+
|
54
|
+
# These are optional methods that strategy can implement to track states between executions
|
55
|
+
def getState(self) -> Dict[str, int | float | bool]:
|
56
|
+
return self.state
|
57
|
+
|
58
|
+
def restoreState(self, state: Dict[str, int | float | bool]) -> None:
|
59
|
+
self.state = state
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from investfly.models.CommonModels import DatedValue, TimeUnit, TimeDelta, Session
|
2
|
+
from investfly.models.Indicator import ParamType, IndicatorParamSpec, IndicatorValueType, IndicatorSpec, Indicator
|
3
|
+
from investfly.models.MarketData import SecurityType, Security, Quote, BarInterval, Bar
|
4
|
+
from investfly.models.MarketDataIds import QuoteField, FinancialField, StandardIndicatorId
|
5
|
+
from investfly.models.PortfolioModels import PositionType, TradeType, Broker, TradeOrder, OrderStatus, PendingOrder, Balances, CompletedTrade, OpenPosition, ClosedPosition, Portfolio, PortfolioPerformance
|
6
|
+
from investfly.models.SecurityUniverseSelector import StandardSymbolsList, CustomSecurityList, SecurityUniverseType, SecurityUniverseSelector, FinancialQuery, FinancialCondition, ComparisonOperator
|
7
|
+
from investfly.models.StrategyModels import DataParams, ScheduleInterval, Schedule, TradeSignal, StandardCloseCriteria, PortfolioSecurityAllocator
|
8
|
+
from investfly.models.TradingStrategy import TradingStrategy
|
9
|
+
from investfly.models.SecurityFilterModels import DataType, DataParam, DataSource
|
10
|
+
|
File without changes
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# This is a self-documenting starter template to define custom indicators in Python Programming Language
|
2
|
+
|
3
|
+
# Following two imports are required
|
4
|
+
from investfly.models import *
|
5
|
+
from investfly.utils import *
|
6
|
+
|
7
|
+
# Import basic types, they aren't required but recommended
|
8
|
+
from typing import Any, List, Dict
|
9
|
+
|
10
|
+
# Following numeric analysis imports are allowed
|
11
|
+
import math
|
12
|
+
import statistics
|
13
|
+
import numpy as np
|
14
|
+
import talib
|
15
|
+
import pandas
|
16
|
+
|
17
|
+
# ! WARN ! Imports other than listed above are disallowed and won't pass validation
|
18
|
+
|
19
|
+
# Create a class that extends Indicator. The class name becomes the "IndicatorId", which must be globally unique
|
20
|
+
class SMAHeikin_CHANGE_ME(Indicator):
|
21
|
+
|
22
|
+
# At minimum, you must implement two methods (1) getIndicatorSpec and (2) shown below.
|
23
|
+
def getIndicatorSpec(self) -> IndicatorSpec:
|
24
|
+
# In this method, you must construct and return IndicatorSpec object that specifies
|
25
|
+
# indicator name, description and any parameters it needs.
|
26
|
+
|
27
|
+
# This information is used by Investfly UI to display information about this indicator in the expression builder
|
28
|
+
|
29
|
+
indicator = IndicatorSpec("[Change Me]: SMA Heikin Ashi")
|
30
|
+
indicator.description = "[ChangeMe]: SMA based on Heikin Ashi Candles"
|
31
|
+
|
32
|
+
# This indicates that the indicator value will have the same unit as stock price and can be plotted
|
33
|
+
# in the same y-axis as stock price as overlay. If the indicator results in a unit-less number like
|
34
|
+
# ADX, you will set it to IndicatorValueType.NUMBER. Other possible values are PERCENT, RATIO, BOOLEAN
|
35
|
+
indicator.valueType = IndicatorValueType.PRICE
|
36
|
+
|
37
|
+
# Specify indicator parameters. For each parameter, you must provide IndicatorParamSpec with
|
38
|
+
# paramType: one of [ParamType.INTEGER, ParamType.FLOAT, ParamType.STRING, ParamType.BOOLEAN]
|
39
|
+
# The remaining properties of ParamSpec are optional
|
40
|
+
# required: [True|False], defaults to True
|
41
|
+
# options:List[Any]: List of valid values . This will make the parameter input widget appear as a dropdown
|
42
|
+
indicator.addParam("period", IndicatorParamSpec(paramType=ParamType.INTEGER, options=IndicatorParamSpec.PERIOD_VALUES))
|
43
|
+
return indicator
|
44
|
+
|
45
|
+
def computeSeries(self, params: Dict[str, Any], bars: List[Bar]) -> List[DatedValue]:
|
46
|
+
# In this method, you compute indicator values for provided parameter values and given data (bars in this case)
|
47
|
+
# For use in strategy, only the latest indicator value is required, but you must compute full series of historical
|
48
|
+
# values so that this indicator can be plotted in the chart and we can run also backtest strategy when this
|
49
|
+
# indicator is used in trading strategy
|
50
|
+
|
51
|
+
# In this template, we will calculate SMA, but using Heikin Ashi candles
|
52
|
+
heikinCandles = CommonUtils.toHeikinAshi(bars)
|
53
|
+
|
54
|
+
dates, close = CommonUtils.extractCloseSeries(heikinCandles)
|
55
|
+
sma_period = params['period']
|
56
|
+
|
57
|
+
# talib requires numpy.array instead of Python arrays, so wrap the close array into np.array
|
58
|
+
smaSeries = talib.SMA(np.array(close), timeperiod=sma_period)
|
59
|
+
|
60
|
+
# Converts the returned numpy.array into List[DatedValue]
|
61
|
+
return CommonUtils.createListOfDatedValue(dates, smaSeries)
|
62
|
+
|
63
|
+
|
64
|
+
# +++++++ The following methods are optional ******
|
65
|
+
|
66
|
+
def dataCountToComputeCurrentValue(self, params: Dict[str, Any]) -> int | None:
|
67
|
+
# As stated above, we only need the latest indicator value while evaluating strategy in real-time.
|
68
|
+
# Here, you return how many input data points are needed to compute just the latest indicator value.
|
69
|
+
# For e.g, to compute SMA(5), you need 5 data points. The return value from this method determines
|
70
|
+
# the length of input data that is passed to computeSeries method above when this indicator evaluates
|
71
|
+
# in real-time execution mode. This speeds up computation significantly by avoiding un-necessary computation
|
72
|
+
# The base class (Indicator) contains implementation of this method that tries to make the best guess,
|
73
|
+
# but it is highly recommended to override this method
|
74
|
+
sma_period = params['period']
|
75
|
+
return sma_period
|
76
|
+
|
77
|
+
def getDataSourceType(self) -> DataSource:
|
78
|
+
# Currenly, only bars are supported. But in the future, we will support defining indicators based on alternate data sources
|
79
|
+
# such as news feed etc. The default is DataSource.BARS
|
80
|
+
return DataSource.BARS
|