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.
Files changed (38) hide show
  1. investfly/__init__.py +0 -0
  2. investfly/api/InvestflyApiClient.py +23 -0
  3. investfly/api/MarketDataApiClient.py +16 -0
  4. investfly/api/PortfolioApiClient.py +37 -0
  5. investfly/api/RestApiClient.py +81 -0
  6. investfly/api/__init__.py +0 -0
  7. investfly/cli/CliApiClient.py +46 -0
  8. investfly/cli/__init__.py +0 -0
  9. investfly/cli/commands.py +78 -0
  10. investfly/models/CommonModels.py +70 -0
  11. investfly/models/Indicator.py +164 -0
  12. investfly/models/MarketData.py +177 -0
  13. investfly/models/MarketDataIds.py +119 -0
  14. investfly/models/ModelUtils.py +34 -0
  15. investfly/models/PortfolioModels.py +270 -0
  16. investfly/models/SecurityFilterModels.py +167 -0
  17. investfly/models/SecurityUniverseSelector.py +202 -0
  18. investfly/models/StrategyModels.py +124 -0
  19. investfly/models/TradingStrategy.py +59 -0
  20. investfly/models/__init__.py +10 -0
  21. investfly/samples/__init__.py +0 -0
  22. investfly/samples/indicators/IndicatorTemplate.py +80 -0
  23. investfly/samples/indicators/NewsSentiment.py +41 -0
  24. investfly/samples/indicators/RsiOfSma.py +42 -0
  25. investfly/samples/indicators/SmaEmaAverage.py +40 -0
  26. investfly/samples/indicators/__init__.py +0 -0
  27. investfly/samples/strategies/SmaCrossOverStrategy.py +35 -0
  28. investfly/samples/strategies/SmaCrossOverTemplate.py +131 -0
  29. investfly/samples/strategies/__init__.py +0 -0
  30. investfly/utils/CommonUtils.py +79 -0
  31. investfly/utils/PercentBasedPortfolioAllocator.py +35 -0
  32. investfly/utils/__init__.py +2 -0
  33. investfly_sdk-1.0.dist-info/LICENSE.txt +21 -0
  34. investfly_sdk-1.0.dist-info/METADATA +53 -0
  35. investfly_sdk-1.0.dist-info/RECORD +38 -0
  36. investfly_sdk-1.0.dist-info/WHEEL +5 -0
  37. investfly_sdk-1.0.dist-info/entry_points.txt +2 -0
  38. 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