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.
- investfly_sdk-1.0/LICENSE.txt +21 -0
- investfly_sdk-1.0/PKG-INFO +53 -0
- investfly_sdk-1.0/README.md +5 -0
- investfly_sdk-1.0/investfly/__init__.py +0 -0
- investfly_sdk-1.0/investfly/api/InvestflyApiClient.py +23 -0
- investfly_sdk-1.0/investfly/api/MarketDataApiClient.py +16 -0
- investfly_sdk-1.0/investfly/api/PortfolioApiClient.py +37 -0
- investfly_sdk-1.0/investfly/api/RestApiClient.py +81 -0
- investfly_sdk-1.0/investfly/api/__init__.py +0 -0
- investfly_sdk-1.0/investfly/cli/CliApiClient.py +46 -0
- investfly_sdk-1.0/investfly/cli/__init__.py +0 -0
- investfly_sdk-1.0/investfly/cli/commands.py +78 -0
- investfly_sdk-1.0/investfly/models/CommonModels.py +70 -0
- investfly_sdk-1.0/investfly/models/Indicator.py +164 -0
- investfly_sdk-1.0/investfly/models/MarketData.py +177 -0
- investfly_sdk-1.0/investfly/models/MarketDataIds.py +119 -0
- investfly_sdk-1.0/investfly/models/ModelUtils.py +34 -0
- investfly_sdk-1.0/investfly/models/PortfolioModels.py +270 -0
- investfly_sdk-1.0/investfly/models/SecurityFilterModels.py +167 -0
- investfly_sdk-1.0/investfly/models/SecurityUniverseSelector.py +202 -0
- investfly_sdk-1.0/investfly/models/StrategyModels.py +124 -0
- investfly_sdk-1.0/investfly/models/TradingStrategy.py +59 -0
- investfly_sdk-1.0/investfly/models/__init__.py +10 -0
- investfly_sdk-1.0/investfly/samples/__init__.py +0 -0
- investfly_sdk-1.0/investfly/samples/indicators/IndicatorTemplate.py +80 -0
- investfly_sdk-1.0/investfly/samples/indicators/NewsSentiment.py +41 -0
- investfly_sdk-1.0/investfly/samples/indicators/RsiOfSma.py +42 -0
- investfly_sdk-1.0/investfly/samples/indicators/SmaEmaAverage.py +40 -0
- investfly_sdk-1.0/investfly/samples/indicators/__init__.py +0 -0
- investfly_sdk-1.0/investfly/samples/strategies/SmaCrossOverStrategy.py +35 -0
- investfly_sdk-1.0/investfly/samples/strategies/SmaCrossOverTemplate.py +131 -0
- investfly_sdk-1.0/investfly/samples/strategies/__init__.py +0 -0
- investfly_sdk-1.0/investfly/utils/CommonUtils.py +79 -0
- investfly_sdk-1.0/investfly/utils/PercentBasedPortfolioAllocator.py +35 -0
- investfly_sdk-1.0/investfly/utils/__init__.py +2 -0
- investfly_sdk-1.0/investfly_sdk.egg-info/PKG-INFO +53 -0
- investfly_sdk-1.0/investfly_sdk.egg-info/SOURCES.txt +41 -0
- investfly_sdk-1.0/investfly_sdk.egg-info/dependency_links.txt +1 -0
- investfly_sdk-1.0/investfly_sdk.egg-info/entry_points.txt +2 -0
- investfly_sdk-1.0/investfly_sdk.egg-info/requires.txt +14 -0
- investfly_sdk-1.0/investfly_sdk.egg-info/top_level.txt +1 -0
- investfly_sdk-1.0/pyproject.toml +40 -0
- 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
|
+
|
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
|
+
|