investfly-sdk 1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. investfly_sdk-1.0/LICENSE.txt +21 -0
  2. investfly_sdk-1.0/PKG-INFO +53 -0
  3. investfly_sdk-1.0/README.md +5 -0
  4. investfly_sdk-1.0/investfly/__init__.py +0 -0
  5. investfly_sdk-1.0/investfly/api/InvestflyApiClient.py +23 -0
  6. investfly_sdk-1.0/investfly/api/MarketDataApiClient.py +16 -0
  7. investfly_sdk-1.0/investfly/api/PortfolioApiClient.py +37 -0
  8. investfly_sdk-1.0/investfly/api/RestApiClient.py +81 -0
  9. investfly_sdk-1.0/investfly/api/__init__.py +0 -0
  10. investfly_sdk-1.0/investfly/cli/CliApiClient.py +46 -0
  11. investfly_sdk-1.0/investfly/cli/__init__.py +0 -0
  12. investfly_sdk-1.0/investfly/cli/commands.py +78 -0
  13. investfly_sdk-1.0/investfly/models/CommonModels.py +70 -0
  14. investfly_sdk-1.0/investfly/models/Indicator.py +164 -0
  15. investfly_sdk-1.0/investfly/models/MarketData.py +177 -0
  16. investfly_sdk-1.0/investfly/models/MarketDataIds.py +119 -0
  17. investfly_sdk-1.0/investfly/models/ModelUtils.py +34 -0
  18. investfly_sdk-1.0/investfly/models/PortfolioModels.py +270 -0
  19. investfly_sdk-1.0/investfly/models/SecurityFilterModels.py +167 -0
  20. investfly_sdk-1.0/investfly/models/SecurityUniverseSelector.py +202 -0
  21. investfly_sdk-1.0/investfly/models/StrategyModels.py +124 -0
  22. investfly_sdk-1.0/investfly/models/TradingStrategy.py +59 -0
  23. investfly_sdk-1.0/investfly/models/__init__.py +10 -0
  24. investfly_sdk-1.0/investfly/samples/__init__.py +0 -0
  25. investfly_sdk-1.0/investfly/samples/indicators/IndicatorTemplate.py +80 -0
  26. investfly_sdk-1.0/investfly/samples/indicators/NewsSentiment.py +41 -0
  27. investfly_sdk-1.0/investfly/samples/indicators/RsiOfSma.py +42 -0
  28. investfly_sdk-1.0/investfly/samples/indicators/SmaEmaAverage.py +40 -0
  29. investfly_sdk-1.0/investfly/samples/indicators/__init__.py +0 -0
  30. investfly_sdk-1.0/investfly/samples/strategies/SmaCrossOverStrategy.py +35 -0
  31. investfly_sdk-1.0/investfly/samples/strategies/SmaCrossOverTemplate.py +131 -0
  32. investfly_sdk-1.0/investfly/samples/strategies/__init__.py +0 -0
  33. investfly_sdk-1.0/investfly/utils/CommonUtils.py +79 -0
  34. investfly_sdk-1.0/investfly/utils/PercentBasedPortfolioAllocator.py +35 -0
  35. investfly_sdk-1.0/investfly/utils/__init__.py +2 -0
  36. investfly_sdk-1.0/investfly_sdk.egg-info/PKG-INFO +53 -0
  37. investfly_sdk-1.0/investfly_sdk.egg-info/SOURCES.txt +41 -0
  38. investfly_sdk-1.0/investfly_sdk.egg-info/dependency_links.txt +1 -0
  39. investfly_sdk-1.0/investfly_sdk.egg-info/entry_points.txt +2 -0
  40. investfly_sdk-1.0/investfly_sdk.egg-info/requires.txt +14 -0
  41. investfly_sdk-1.0/investfly_sdk.egg-info/top_level.txt +1 -0
  42. investfly_sdk-1.0/pyproject.toml +40 -0
  43. investfly_sdk-1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023+ Investfly, Finverse LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.1
2
+ Name: investfly-sdk
3
+ Version: 1.0
4
+ Summary: Investfly SDK
5
+ Author-email: "Investfly.com" <admin@investfly.com>
6
+ License: The MIT License (MIT)
7
+
8
+ Copyright (c) 2023+ Investfly, Finverse LLC
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://www.investfly.com
28
+ Classifier: Programming Language :: Python :: 3
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Operating System :: OS Independent
31
+ Requires-Python: >=3.7
32
+ Description-Content-Type: text/markdown
33
+ License-File: LICENSE.txt
34
+ Requires-Dist: certifi==2023.7.22
35
+ Requires-Dist: charset-normalizer==3.2.0
36
+ Requires-Dist: idna==3.4
37
+ Requires-Dist: pandas==2.0.3
38
+ Requires-Dist: pandas-stubs==2.0.3.230814
39
+ Requires-Dist: pandas-ta==0.3.14b0
40
+ Requires-Dist: python-dateutil==2.8.2
41
+ Requires-Dist: pytz==2023.3
42
+ Requires-Dist: requests==2.31.0
43
+ Requires-Dist: types-requests==2.31.0.2
44
+ Requires-Dist: six==1.16.0
45
+ Requires-Dist: tzdata==2023.3
46
+ Requires-Dist: urllib3==1.26.15
47
+ Requires-Dist: numpy==1.26.4
48
+
49
+ # About
50
+
51
+ Python-SDK to work with Investfly API.
52
+ [Investfly](https://www.investfly.com)
53
+
@@ -0,0 +1,5 @@
1
+ # About
2
+
3
+ Python-SDK to work with Investfly API.
4
+ [Investfly](https://www.investfly.com)
5
+
File without changes
@@ -0,0 +1,23 @@
1
+ import datetime
2
+
3
+ from investfly.api.MarketDataApiClient import MarketDataApiClient
4
+ from investfly.api.PortfolioApiClient import PortfolioApiClient
5
+ from investfly.api.RestApiClient import RestApiClient
6
+
7
+
8
+ class InvestflyApiClient:
9
+
10
+ def __init__(self, baseUrl: str = "https://api.investfly.com"):
11
+ self.restApiClient = RestApiClient(baseUrl)
12
+ self.marketApi = MarketDataApiClient(self.restApiClient)
13
+ self.portfolioApi = PortfolioApiClient(self.restApiClient)
14
+
15
+ def login(self, username, password):
16
+ return self.restApiClient.login(username, password)
17
+
18
+ def logout(self):
19
+ self.restApiClient.logout()
20
+
21
+ @staticmethod
22
+ def parseDatetime(date_str: str) -> datetime.datetime:
23
+ return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S.%f%z')
@@ -0,0 +1,16 @@
1
+ from typing import Set, List, Dict, Any
2
+
3
+ from investfly.api.RestApiClient import RestApiClient
4
+
5
+
6
+ class MarketDataApiClient:
7
+
8
+ def __init__(self, restApiClient: RestApiClient) -> None:
9
+ self.restApiClient = restApiClient
10
+
11
+ def getNews(self, symbol: str) -> List[Dict[str, Any]]:
12
+ return self.restApiClient.doGet(f"/symbol/news?symbols={symbol}")
13
+
14
+
15
+ def getStandardSymbols(self, standardListName: str) -> Set[str]:
16
+ return {"AAPL"}
@@ -0,0 +1,37 @@
1
+ from typing import Any, Dict
2
+
3
+ from investfly.api.RestApiClient import RestApiClient
4
+ from investfly.models.PortfolioModels import Broker, Portfolio, Balances, OpenPosition, CompletedTrade, PendingOrder, \
5
+ TradeOrder, OrderStatus
6
+
7
+
8
+ class PortfolioApiClient:
9
+
10
+ def __init__(self, restApiClient: RestApiClient) -> None:
11
+ self.restApiClient = restApiClient
12
+
13
+ def getPortfolio(self, portfolioId: str, broker: Broker) -> Portfolio:
14
+ portfolioDict: Dict[str, Any] = self.restApiClient.doGet(f"/portfolio/{broker.value}/{portfolioId}")
15
+ balances = Balances.fromDict(portfolioDict)
16
+ portfolio = Portfolio(portfolioDict["portfolioId"], Broker(portfolioDict["broker"]), balances)
17
+
18
+ openPosList = self.restApiClient.doGet(f'/portfolio/{broker.value}/{portfolioId}/portfoliostocks')
19
+ for openPosDict in openPosList:
20
+ portfolio.openPositions.append(OpenPosition.fromDict(openPosDict))
21
+
22
+ completedTradesList = self.restApiClient.doGet(f'/portfolio/{broker.value}/{portfolioId}/trades')
23
+ for compTradeDict in completedTradesList:
24
+ portfolio.completedTrades.append(CompletedTrade.fromDict(compTradeDict))
25
+
26
+ pendingOrdersList = self.restApiClient.doGet(f'/portfolio/{broker.value}/{portfolioId}/pending')
27
+ for pendingOrderDict in pendingOrdersList:
28
+ portfolio.pendingOrders.append(PendingOrder.fromDict(pendingOrderDict))
29
+
30
+ return portfolio
31
+
32
+ def submitTradeOrder(self, portfolioId: str, broker: Broker, order: TradeOrder) -> OrderStatus:
33
+ res = self.restApiClient.doPost(f'/portfolio/{broker.value}/{portfolioId}/trade', order.toDict())
34
+ return OrderStatus.fromDict(res)
35
+
36
+
37
+
@@ -0,0 +1,81 @@
1
+ import logging
2
+ import warnings
3
+ from typing import Dict, Any
4
+
5
+ import requests
6
+ from requests import Response
7
+
8
+ from investfly.models.CommonModels import Session
9
+
10
+ warnings.simplefilter("ignore")
11
+
12
+
13
+ class RestApiClient:
14
+
15
+ def __init__(self, baseUrl: str) -> None:
16
+ self.headers: Dict[str, str] = {}
17
+ self.baseUrl = baseUrl
18
+ self.log = logging.getLogger(self.__class__.__name__)
19
+
20
+ def login(self, username: str, password: str) -> Session:
21
+ res = requests.post(self.baseUrl + "/user/login", auth=(username, password), verify=False)
22
+ if res.status_code == 200:
23
+ self.headers['investfly-client-id'] = res.headers['investfly-client-id']
24
+ self.headers['investfly-client-token'] = res.headers['investfly-client-token']
25
+ dict_obj = res.json()
26
+ session = Session.fromJsonDict(dict_obj)
27
+ return session
28
+ else:
29
+ raise RestApiClient.getException(res)
30
+
31
+ def logout(self):
32
+ requests.post(self.baseUrl + "/user/logout", verify=False)
33
+ self.clientId = None
34
+ self.clientToken = None
35
+
36
+ def doGet(self, url: str) -> Any:
37
+ res = requests.get(self.baseUrl + url, headers=self.headers, verify=False)
38
+ # This does not actually return JSON string, but instead returns Python Dictionary/List etc
39
+ if res.status_code == 200:
40
+ contentType: str = res.headers['Content-Type']
41
+ if "json" in contentType:
42
+ return res.json()
43
+ else:
44
+ return res.text
45
+ else:
46
+ raise RestApiClient.getException(res)
47
+
48
+ def doPost(self, url: str, obj: Dict[str, Any]) -> Any:
49
+ res: Response = requests.post(self.baseUrl + url, json=obj, headers=self.headers, verify=False)
50
+ if res.status_code == 200:
51
+ contentType: str = res.headers['Content-Type']
52
+ if "json" in contentType:
53
+ return res.json()
54
+ else:
55
+ return res.text
56
+ else:
57
+ raise RestApiClient.getException(res)
58
+
59
+ def doPostCode(self, url: str, code: str) -> Any:
60
+ res: Response = requests.post(self.baseUrl + url, data=code, headers=self.headers, verify=False)
61
+ if res.status_code == 200:
62
+ contentType: str = res.headers['Content-Type']
63
+ if "json" in contentType:
64
+ return res.json()
65
+ else:
66
+ return res.text
67
+ else:
68
+ raise RestApiClient.getException(res)
69
+
70
+ @staticmethod
71
+ def getException(res: Response):
72
+ try:
73
+ # Server returns valid JSON in case of any exceptions that may occor while processing request
74
+ errorObj: Dict[str, Any] = res.json()
75
+ if 'message' in errorObj.keys():
76
+ return Exception(errorObj.get('message'))
77
+ else:
78
+ return Exception(str(errorObj))
79
+ except requests.exceptions.JSONDecodeError:
80
+ # Just in case, there are other errors
81
+ return Exception(res.text)
File without changes
@@ -0,0 +1,46 @@
1
+ import requests
2
+ from investfly.api.RestApiClient import RestApiClient
3
+
4
+ class CliApiClient:
5
+
6
+ def __init__(self, baseUrl: str) -> None:
7
+ self.restApi = RestApiClient(baseUrl)
8
+
9
+ def login(self, username: str, password: str):
10
+ try:
11
+ user = self.restApi.login(username, password)
12
+ print("Successfully logged in as: "+user.username)
13
+ except Exception as e:
14
+ print(e)
15
+
16
+ def logout(self):
17
+ self.restApi.logout()
18
+
19
+ def getStatus(self):
20
+ try:
21
+ userInfo = self.restApi.doGet('/user/session')
22
+ print("Currently logged in as "+userInfo['username'])
23
+ except Exception as e:
24
+ print(e)
25
+
26
+ def getStrategies(self):
27
+ try:
28
+ strategies = self.restApi.doGet('/strategy/list')
29
+ for strategy in strategies:
30
+ print(str(strategy['strategyId'])+'\t'+strategy['strategyName']+'\n'+strategy['strategyDesc']+'\n')
31
+ except Exception as e:
32
+ print(e)
33
+
34
+ def saveStrategy(self, strategyId: int):
35
+ try:
36
+ strategy = self.restApi.doGet('/strategy/'+str(strategyId))
37
+ return strategy['pythonCode']
38
+ except Exception as e:
39
+ return e
40
+
41
+ def updateStrategy(self, id: int, code: str):
42
+ try:
43
+ self.restApi.doPostCode('/strategy/'+id+'/update/code', code)
44
+ print('Strategy successfully updated')
45
+ except Exception as e:
46
+ print(e)
File without changes
@@ -0,0 +1,78 @@
1
+ import argparse
2
+ import pickle
3
+ import os.path
4
+
5
+ from investfly.cli.CliApiClient import CliApiClient
6
+
7
+ def main():
8
+ # Check to see if user already has a session active
9
+ if os.path.exists('/tmp/loginSession'):
10
+ tmpfile = open('/tmp/loginSession', 'rb')
11
+ restApi = pickle.load(tmpfile)
12
+ tmpfile.close()
13
+ else:
14
+ restApi = CliApiClient("https://api.investfly.com")
15
+
16
+ # CLI Commands
17
+ parser = argparse.ArgumentParser()
18
+ subparser = parser.add_subparsers(dest='command')
19
+
20
+ parser_login = subparser.add_parser('login', help='Login to Investfly')
21
+ parser_login.add_argument('-u', '--username', required='true', help='Input username')
22
+ parser_login.add_argument('-p', '--password', required='true', help='Input user password')
23
+
24
+ subparser.add_parser('whoami', help='View your user information')
25
+
26
+ subparser.add_parser('logout', help='Logout')
27
+
28
+ parser_strategy = subparser.add_parser('strategy', help='View all your current strategies')
29
+ parser_strategy.add_argument('-id', help='Provide the Strategy ID')
30
+ parser_strategy.add_argument('-o', '--output-file', help='Provide a location to save the output file of a custom strategy (Requires Strategy ID)')
31
+ parser_strategy.add_argument('-u', '--update-file', help='Provide the file location to update the script of a custom strategy (Requires Strategy ID)')
32
+
33
+ args = parser.parse_args()
34
+
35
+ # If user is logging in, create a new login session and save it locally
36
+ if args.command == 'login':
37
+ restApi.login(args.username, args.password)
38
+ tmpFile = open('/tmp/loginSession', 'ab')
39
+ pickle.dump(restApi, tmpFile)
40
+ tmpFile.close()
41
+
42
+ elif args.command == 'logout':
43
+ restApi.logout()
44
+ os.remove('/tmp/loginSession')
45
+
46
+ elif args.command == 'whoami':
47
+ restApi.getStatus()
48
+
49
+ elif args.command == 'strategy':
50
+ if all(e is None for e in [args.id, args.output_file, args.update_file]):
51
+ restApi.getStrategies()
52
+ elif (args.output_file is not None) and (args.id is not None):
53
+ try:
54
+ code = restApi.saveStrategy(args.id)
55
+ file = open(args.output_file, "w")
56
+ file.write(code)
57
+ file.close()
58
+ print('File successfully saved to '+args.output_file)
59
+ except Exception as e:
60
+ print(e)
61
+ elif (args.update_file is not None) and (args.id is not None):
62
+ try:
63
+ file = open(args.update_file, "r")
64
+ code = file.read()
65
+ restApi.updateStrategy(args.id, code)
66
+ file.close()
67
+ except Exception as e:
68
+ print(e)
69
+ else:
70
+ parser_strategy.print_help()
71
+
72
+
73
+ else:
74
+ parser.print_help()
75
+
76
+
77
+ if __name__ == '__main__':
78
+ main()
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import timedelta, datetime
5
+ from enum import Enum
6
+ from typing import Any, Dict
7
+
8
+ from investfly.models.ModelUtils import ModelUtils
9
+
10
+
11
+ @dataclass
12
+ class DatedValue:
13
+ date: datetime
14
+ value: float | int
15
+
16
+ def toJsonDict(self) -> Dict[str, Any]:
17
+ return {
18
+ 'date': ModelUtils.formatDatetime(self.date),
19
+ 'value': self.value
20
+ }
21
+
22
+ @staticmethod
23
+ def fromDict(json_dict: Dict[str, Any]) -> DatedValue:
24
+ return DatedValue(ModelUtils.parseDatetime(json_dict['date']), json_dict['value'])
25
+
26
+
27
+ class TimeUnit(str, Enum):
28
+ MINUTES = "MINUTES"
29
+ HOURS = "HOURS"
30
+ DAYS = "DAYS"
31
+
32
+ def __str__(self):
33
+ return self.value
34
+
35
+ def __repr__(self):
36
+ return self.value
37
+
38
+
39
+ @dataclass
40
+ class TimeDelta:
41
+ value: int
42
+ unit: TimeUnit
43
+
44
+ def toPyTimeDelta(self) -> timedelta:
45
+ totalMinutes = self.value
46
+ if self.unit == TimeUnit.HOURS:
47
+ totalMinutes = totalMinutes * 60
48
+ elif self.unit == TimeUnit.DAYS:
49
+ totalMinutes = totalMinutes * 60 * 24
50
+
51
+ return timedelta(minutes=totalMinutes)
52
+
53
+ def toDict(self) -> Dict[str, Any]:
54
+ return self.__dict__.copy()
55
+
56
+ @staticmethod
57
+ def fromDict(json_dict: Dict[str, Any]) -> TimeDelta:
58
+ return TimeDelta(json_dict['value'], TimeUnit[json_dict['unit']])
59
+
60
+
61
+
62
+ @dataclass
63
+ class Session:
64
+ username: str
65
+ clientId: str
66
+ clientToken: str
67
+
68
+ @staticmethod
69
+ def fromJsonDict(json_dict: Dict[str, Any]) -> Session:
70
+ return Session(json_dict['username'], json_dict['clientId'], json_dict['clientToken'])
@@ -0,0 +1,164 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from typing import Any, Dict, List, ClassVar
5
+
6
+ from investfly.models.CommonModels import DatedValue
7
+ from investfly.models.MarketData import BarInterval
8
+ from investfly.models.SecurityFilterModels import DataSource, DataParam
9
+
10
+
11
+ class ParamType(str, Enum):
12
+ INTEGER = 'INTEGER'
13
+ FLOAT = 'FLOAT'
14
+ BOOLEAN = 'BOOLEAN'
15
+ STRING = 'STRING'
16
+ BARINTERVAL = 'BARINTERVAL'
17
+
18
+ def __str__(self):
19
+ return self.value
20
+
21
+ def __repr__(self):
22
+ return self.value
23
+
24
+
25
+ @dataclass
26
+ class IndicatorParamSpec:
27
+ paramType: ParamType
28
+
29
+ required: bool = True
30
+ # The default value here is just a "hint" to the UI to auto-fill indicator value with reasonable default
31
+ defaultValue: Any | None = None
32
+ options: List[Any] | None = None
33
+
34
+ PERIOD_VALUES: ClassVar[List[int]] = [2, 3, 4, 5, 8, 9, 10, 12, 14, 15, 20, 26, 30, 40, 50, 60, 70, 80, 90, 100, 120, 130, 140, 150, 180, 200, 250, 300]
35
+
36
+ def toDict(self) -> Dict[str, Any]:
37
+ d = self.__dict__.copy()
38
+ return d
39
+
40
+
41
+ class IndicatorValueType(str, Enum):
42
+ # Indicator ValueType can possibly used by Investfly to validate expression and optimize experience for users
43
+ # For e.g, all Indicators of same valueType can be plotted in the same y-axis
44
+
45
+ PRICE = "PRICE"
46
+
47
+ # Values that ranges from 0-100
48
+ PERCENT = "PERCENT"
49
+
50
+ # Values that ranges from 0-1
51
+ RATIO = "RATIO"
52
+
53
+ # The value must be 0 or 1
54
+ BOOLEAN = "BOOLEAN"
55
+
56
+ # For arbitrary numeric value, use NUMBER, which is also the default
57
+ NUMBER = "NUMBER"
58
+
59
+ def __str__(self):
60
+ return self.value
61
+
62
+ def __repr__(self):
63
+ return self.value
64
+
65
+ # IndicatorDefinition represents indicator used in Investfly. Each indicator implementation must provide
66
+ # IndicatorDefinition, that specifies its name, description, required parameters and what type of value
67
+ # it returns.
68
+
69
+ class IndicatorSpec:
70
+
71
+ def __init__(self, name: str) -> None:
72
+ # indicatorId is automatically set to clazz name of the indicator implementation
73
+ self.indicatorId: str
74
+
75
+ self.name: str = name
76
+
77
+ # Description is defaulted to name for simplicity but can be set properly after instantiation
78
+ self.description: str = name
79
+
80
+ self.valueType: IndicatorValueType = IndicatorValueType.NUMBER
81
+ self.params: Dict[str, IndicatorParamSpec] = {}
82
+
83
+ def addParam(self, paramName: str, paramSpec: IndicatorParamSpec) -> None:
84
+ self.params[paramName] = paramSpec
85
+
86
+ def toJsonDict(self) -> Dict[str, Any]:
87
+ jsonDict = self.__dict__.copy()
88
+ # IndicatorParamSpec.toDict() must be called
89
+ paramsDict = {}
90
+ for paramName in self.params.keys():
91
+ paramsDict[paramName] = self.params[paramName].toDict()
92
+ jsonDict['params'] = paramsDict
93
+ return jsonDict
94
+
95
+ def __str__(self):
96
+ return str(self.__dict__)
97
+
98
+
99
+ class Indicator(ABC):
100
+
101
+ @abstractmethod
102
+ def getIndicatorSpec(self) -> IndicatorSpec:
103
+ # Return IndicatorDefinition with name, description, required params, and valuetype
104
+ # See IndicatorDefinition abstract class for more details
105
+ pass
106
+
107
+ def getDataSourceType(self) -> DataSource:
108
+ # Return the DataSource that this indicator is based on. Possible values are:
109
+ # DataSource.BARS, DataSource.QUOTE, DataSource.NEWS, DataSource.FINANCIAL
110
+ return DataSource.BARS
111
+
112
+ @abstractmethod
113
+ def computeSeries(self, params: Dict[str, Any], data: List[Any]) -> List[DatedValue]:
114
+ # Indicator series is needed for backtest and plotting on stock chart
115
+ pass
116
+
117
+ def validateParams(self, paramVals: Dict[str, Any]):
118
+ spec: IndicatorSpec = self.getIndicatorSpec()
119
+ for paramName, paramSpec in spec.params.items():
120
+ paramVal: Any = paramVals.get(paramName)
121
+ expectedParamType = paramSpec.paramType
122
+
123
+ if paramVal is not None:
124
+ if expectedParamType == ParamType.INTEGER and not isinstance(paramVal, int):
125
+ raise Exception(f"Param {paramName} must be of type int. You provided: {paramVal}")
126
+ if expectedParamType == ParamType.FLOAT and not isinstance(paramVal, float) and isinstance(paramVal,int):
127
+ raise Exception(f"Param {paramName} must be of type float. You provided: {paramVal}")
128
+ if expectedParamType == ParamType.STRING and not isinstance(paramVal, str):
129
+ raise Exception(f"Param {paramName} must be of type string. You provided: {paramVal}")
130
+ if expectedParamType == ParamType.BOOLEAN and not isinstance(paramVal, bool):
131
+ raise Exception(f"Param {paramName} must be of type boolean. You provided: {paramVal}")
132
+ if expectedParamType == ParamType.BOOLEAN and not isinstance(paramVal, bool):
133
+ raise Exception(f"Param {paramName} must be of type boolean. You provided: {paramVal}")
134
+ if expectedParamType == ParamType.BARINTERVAL and not isinstance(paramVal, BarInterval):
135
+ raise Exception(f"Param {paramName} must be of type BarInterval. You provided: {paramVal}")
136
+
137
+ if paramSpec.options is not None:
138
+ if paramVal not in paramSpec.options:
139
+ raise Exception(f"Param {paramName} provided value {paramVal} is not one of the allowed value")
140
+
141
+
142
+ def dataCountToComputeCurrentValue(self, params: Dict[str, Any]) -> int | None:
143
+ total = 0
144
+ for key, value in params.items():
145
+ if isinstance(value, int) and key != DataParam.COUNT:
146
+ total += value
147
+ return max(total, 1)
148
+
149
+ def addStandardParamsToDef(self, indicatorDef: IndicatorSpec):
150
+ # Note that setting default values for optional params impact alias/key generation for indicator instances (e.g SMA_5_1MIN_1)
151
+ # Hence, its better to leave them as None
152
+ indicatorDef.params[DataParam.COUNT] = IndicatorParamSpec(ParamType.INTEGER, False, None)
153
+ indicatorDef.params[DataParam.LOOKBACK] = IndicatorParamSpec(ParamType.INTEGER, False, None, [1,2,3,4,5,6,7,8,9,10])
154
+ indicatorDef.params[DataParam.SECURITY] = IndicatorParamSpec(ParamType.STRING, False)
155
+
156
+ # Add datasource dependent parameters
157
+ dataSource = self.getDataSourceType()
158
+ if dataSource == DataSource.BARS:
159
+ indicatorDef.params[DataParam.BARINTERVAL] = IndicatorParamSpec(ParamType.BARINTERVAL, True, BarInterval.ONE_MINUTE, [v for v in BarInterval])
160
+ elif dataSource == DataSource.FINANCIAL or dataSource == DataSource.QUOTE:
161
+ # Its optional because in the absense of field, indicator.computeSeries() will be passed full FinancialDict
162
+ indicatorDef.params[DataParam.FIELD] = IndicatorParamSpec(ParamType.STRING, False)
163
+
164
+