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,177 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from datetime import datetime
|
5
|
+
from enum import Enum
|
6
|
+
from typing import TypedDict, Dict, Any, cast
|
7
|
+
|
8
|
+
from investfly.models.CommonModels import DatedValue
|
9
|
+
from investfly.models.MarketDataIds import QuoteField
|
10
|
+
|
11
|
+
def parseDatetime(date_str: str) -> datetime:
|
12
|
+
return datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S.%f%z')
|
13
|
+
|
14
|
+
|
15
|
+
def formatDatetime(dt: datetime) -> str:
|
16
|
+
return dt.strftime('%Y-%m-%dT%H:%M:%S.%f%z')
|
17
|
+
|
18
|
+
|
19
|
+
class SecurityType(str, Enum):
|
20
|
+
STOCK = "STOCK"
|
21
|
+
ETF = "ETF"
|
22
|
+
|
23
|
+
def __str__(self):
|
24
|
+
return self.value
|
25
|
+
|
26
|
+
def __repr__(self):
|
27
|
+
return self.value
|
28
|
+
|
29
|
+
|
30
|
+
# All enums subclass str to make them JSON serializable
|
31
|
+
|
32
|
+
@dataclass(frozen=True)
|
33
|
+
class Security:
|
34
|
+
symbol: str
|
35
|
+
securityType: SecurityType
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def createStock(symbol: str):
|
39
|
+
return Security(symbol, SecurityType.STOCK)
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def fromDict(jsonDict: Dict[str, Any]) -> Security:
|
43
|
+
return Security(jsonDict["symbol"], SecurityType[jsonDict["securityType"]])
|
44
|
+
|
45
|
+
def toDict(self) -> Dict[str, Any]:
|
46
|
+
return self.__dict__.copy()
|
47
|
+
|
48
|
+
|
49
|
+
@dataclass
|
50
|
+
class Quote:
|
51
|
+
symbol: str
|
52
|
+
date: datetime
|
53
|
+
lastPrice: float
|
54
|
+
prevClose: float| None = None
|
55
|
+
todayChange: float | None = None
|
56
|
+
todayChangePercent: float | None = None
|
57
|
+
dayOpen: float | None = None
|
58
|
+
dayHigh: float | None = None
|
59
|
+
dayLow: float | None = None
|
60
|
+
dayVolume: int | None = None
|
61
|
+
|
62
|
+
@staticmethod
|
63
|
+
def fromDict(jsonDict: Dict[str, Any]) -> Quote:
|
64
|
+
quote = Quote(jsonDict["symbol"], parseDatetime(jsonDict["date"]), jsonDict["lastPrice"])
|
65
|
+
quote.prevClose = jsonDict.get('prevClose')
|
66
|
+
quote.todayChange = jsonDict.get("todayChange")
|
67
|
+
quote.todayChangePercent = jsonDict.get('todayChangePercent')
|
68
|
+
quote.dayOpen = jsonDict.get('dayOpen')
|
69
|
+
quote.dayHigh = jsonDict.get('dayHigh')
|
70
|
+
quote.dayLow = jsonDict.get('dayLow')
|
71
|
+
return quote
|
72
|
+
|
73
|
+
def toDict(self) -> Dict[str, Any]:
|
74
|
+
jsonDict = self.__dict__.copy()
|
75
|
+
jsonDict["date"] = formatDatetime(self.date)
|
76
|
+
return jsonDict
|
77
|
+
|
78
|
+
def toIndicatorValue(self, quoteField: QuoteField) -> DatedValue:
|
79
|
+
if quoteField == QuoteField.LASTPRICE:
|
80
|
+
return DatedValue(self.date, self.lastPrice)
|
81
|
+
elif quoteField == QuoteField.DAY_OPEN:
|
82
|
+
if self.dayOpen is None:
|
83
|
+
raise Exception("DAY_OPEN price not available in Quote")
|
84
|
+
return DatedValue(self.date, self.dayOpen)
|
85
|
+
elif quoteField == QuoteField.DAY_HIGH:
|
86
|
+
if self.dayHigh is None:
|
87
|
+
raise Exception("DAY_HIGH price not available in Quote")
|
88
|
+
return DatedValue(self.date, self.dayHigh)
|
89
|
+
elif quoteField == QuoteField.DAY_LOW:
|
90
|
+
if self.dayLow is None:
|
91
|
+
raise Exception("DAY_LOW price not available in Quote")
|
92
|
+
return DatedValue(self.date, self.dayLow)
|
93
|
+
elif quoteField == QuoteField.PREV_DAY_CLOSE:
|
94
|
+
if self.prevClose is None:
|
95
|
+
raise Exception("PREV_DAY_CLOSE price not available in Quote")
|
96
|
+
return DatedValue(self.date, self.prevClose)
|
97
|
+
elif quoteField == QuoteField.DAY_CHANGE:
|
98
|
+
if self.todayChange is None:
|
99
|
+
raise Exception("DAY_CHANGE not available in Quote")
|
100
|
+
return DatedValue(self.date, self.todayChange)
|
101
|
+
elif quoteField == QuoteField.DAY_CHANGE_PCT:
|
102
|
+
if self.todayChangePercent is None:
|
103
|
+
raise Exception("DAY_CHANGE_PCT value not available in Quote")
|
104
|
+
return DatedValue(self.date, self.todayChangePercent)
|
105
|
+
elif quoteField == QuoteField.DAY_VOLUME:
|
106
|
+
if self.dayVolume is None:
|
107
|
+
raise Exception("DAY_VOLUME not available in Quote")
|
108
|
+
return DatedValue(self.date, self.dayVolume)
|
109
|
+
else:
|
110
|
+
raise Exception("Invalid Quote Indicator ID: " + quoteField)
|
111
|
+
|
112
|
+
def toEODBar(self) -> Bar:
|
113
|
+
bar = Bar()
|
114
|
+
bar['symbol'] = self.symbol
|
115
|
+
bar['barinterval'] = BarInterval.ONE_DAY
|
116
|
+
bar['date'] = self.date.replace(second=0, microsecond=0)
|
117
|
+
bar['open'] = cast(float, self.dayOpen)
|
118
|
+
bar['high'] = cast(float, self.dayHigh)
|
119
|
+
bar['low'] = cast(float, self.dayLow)
|
120
|
+
bar['close'] = cast(float, self.lastPrice)
|
121
|
+
bar['volume'] = cast(int, self.dayVolume)
|
122
|
+
return bar
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
class BarInterval(str, Enum):
|
128
|
+
ONE_MINUTE = "ONE_MINUTE"
|
129
|
+
FIVE_MINUTE = "FIVE_MINUTE"
|
130
|
+
FIFTEEN_MINUTE = "FIFTEEN_MINUTE"
|
131
|
+
THIRTY_MINUTE = "THIRTY_MINUTE"
|
132
|
+
SIXTY_MINUTE = "SIXTY_MINUTE"
|
133
|
+
ONE_DAY = "ONE_DAY"
|
134
|
+
|
135
|
+
def getMinutes(self) -> int:
|
136
|
+
if self == BarInterval.ONE_MINUTE:
|
137
|
+
return 1
|
138
|
+
elif self == BarInterval.FIVE_MINUTE:
|
139
|
+
return 5
|
140
|
+
elif self == BarInterval.FIFTEEN_MINUTE:
|
141
|
+
return 15
|
142
|
+
elif self == BarInterval.THIRTY_MINUTE:
|
143
|
+
return 30
|
144
|
+
elif self == BarInterval.SIXTY_MINUTE:
|
145
|
+
return 60
|
146
|
+
else:
|
147
|
+
return 24 * 60
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
Bar = TypedDict("Bar", {"symbol": str,
|
152
|
+
"barinterval": BarInterval,
|
153
|
+
"date": datetime,
|
154
|
+
"open": float,
|
155
|
+
"close": float,
|
156
|
+
"high": float,
|
157
|
+
"low": float,
|
158
|
+
"volume": int
|
159
|
+
},
|
160
|
+
total=False)
|
161
|
+
|
162
|
+
@dataclass
|
163
|
+
class StockNews:
|
164
|
+
date: datetime
|
165
|
+
title: str
|
166
|
+
description: str
|
167
|
+
|
168
|
+
|
169
|
+
class NoDataException(Exception):
|
170
|
+
def __init__(self, dataParams: Dict[str, Any]):
|
171
|
+
self.message = f"Data Not Available: {dataParams}"
|
172
|
+
|
173
|
+
def __str__(self) -> str:
|
174
|
+
return self.message
|
175
|
+
|
176
|
+
def __repr__(self) -> str:
|
177
|
+
return self.message
|
@@ -0,0 +1,119 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
|
4
|
+
class QuoteField(str, Enum):
|
5
|
+
# ------ Quote ---
|
6
|
+
LASTPRICE = "LASTPRICE"
|
7
|
+
DAY_OPEN = "DAY_OPEN"
|
8
|
+
DAY_HIGH = "DAY_HIGH"
|
9
|
+
DAY_LOW = "DAY_LOW"
|
10
|
+
DAY_VOLUME = "DAY_VOLUME"
|
11
|
+
PREV_DAY_CLOSE = "PREV_DAY_CLOSE"
|
12
|
+
DAY_CHANGE = "DAY_CHANGE"
|
13
|
+
DAY_CHANGE_PCT = "DAY_CHANGE_PCT"
|
14
|
+
DAY_CHANGE_OPEN = "DAY_CHANGE_OPEN"
|
15
|
+
|
16
|
+
class FinancialField(str, Enum):
|
17
|
+
# fundamental metrics below
|
18
|
+
|
19
|
+
# SHARE
|
20
|
+
OUTSTANDING_SHARES = "OUTSTANDING_SHARES"
|
21
|
+
MARKET_CAP = "MARKET_CAP"
|
22
|
+
|
23
|
+
# INCOME STATEMENT
|
24
|
+
REVENUE = "REVENUE"
|
25
|
+
COST_OF_REVENUE = "COST_OF_REVENUE"
|
26
|
+
GROSS_PROFIT = "GROSS_PROFIT"
|
27
|
+
OPERATING_EXPENSE = "OPERATING_EXPENSE"
|
28
|
+
OPERATING_INCOME = "OPERATING_INCOME"
|
29
|
+
NET_INCOME = "NET_INCOME" # subtract taxes to get net income
|
30
|
+
EPS = "EPS"
|
31
|
+
|
32
|
+
# BALANCE SHEET: Assets = Liabilities + Equity
|
33
|
+
CURR_ASSETS = "CURR_ASSETS"
|
34
|
+
NON_CURR_ASSETS = "NON_CURR_ASSETS"
|
35
|
+
ASSETS = "ASSETS"
|
36
|
+
CURR_LIABILITIES = "CURR_LIABILITIES"
|
37
|
+
NON_CURR_LIABILITIES = "NON_CURR_LIABILITIES"
|
38
|
+
LIABILITIES = "LIABILITIES"
|
39
|
+
LONG_TERM_DEBT = "LONG_TERM_DEBT"
|
40
|
+
EQUITY = "EQUITY"
|
41
|
+
|
42
|
+
# CASH FLOW
|
43
|
+
OPEARTING_CASH_FLOW = "OPEARTING_CASH_FLOW"
|
44
|
+
INVESTING_CASH_FLOW = "INVESTING_CASH_FLOW"
|
45
|
+
FINANCING_CASH_FLOW = "FINANCING_CASH_FLOW"
|
46
|
+
NET_CASH_FLOW = "NET_CASH_FLOW"
|
47
|
+
|
48
|
+
RETURN_ON_EQUITY = "RETURN_ON_EQUITY"
|
49
|
+
RETURN_ON_ASSETS = "RETURN_ON_ASSETS"
|
50
|
+
PRICE_TO_EARNINGS_RATIO = "PRICE_TO_EARNINGS_RATIO"
|
51
|
+
PEG_RATIO = "PEG_RATIO"
|
52
|
+
PRICE_TO_SALES_RATIO = "PRICE_TO_SALES_RATIO"
|
53
|
+
PRICE_TO_BOOK_RATIO = "PRICE_TO_BOOK_RATIO"
|
54
|
+
REVENUE_GROWTH = "REVENUE_GROWTH"
|
55
|
+
EPS_GROWTH = "EPS_GROWTH"
|
56
|
+
DEBT_EQUITY_RATIO = "DEBT_EQUITY_RATIO"
|
57
|
+
CURRENT_RATIO = "CURRENT_RATIO"
|
58
|
+
GROSS_MARGIN = "GROSS_MARGIN"
|
59
|
+
PROFIT_MARGIN = "PROFIT_MARGIN"
|
60
|
+
|
61
|
+
DIVIDEND_YIELD = "DIVIDEND_YIELD"
|
62
|
+
|
63
|
+
|
64
|
+
class StandardIndicatorId(str, Enum):
|
65
|
+
|
66
|
+
# Works by default on DAILY bars
|
67
|
+
AVGVOL = "AVGVOLUME"
|
68
|
+
HIGH_52_WEEK = "HIGH52WEEK"
|
69
|
+
LOW_52_WEEK = "LOW52WEEK"
|
70
|
+
|
71
|
+
DRAWDOWN = "DRAWDOWN"
|
72
|
+
PERCENTCHANGE = "PRICECHANGEPCT"
|
73
|
+
|
74
|
+
SMA = "SMA"
|
75
|
+
EMA = "EMA"
|
76
|
+
MACD = "MACD"
|
77
|
+
MACDS = "MACDS"
|
78
|
+
RSI = "RSI"
|
79
|
+
ATR = "ATR"
|
80
|
+
ADX = "ADX"
|
81
|
+
PLUS_DI = "PLUS_DI"
|
82
|
+
MINUS_DI = "MINUS_DI"
|
83
|
+
PSAR = "PSAR"
|
84
|
+
STD_DEV = "STD_DEV"
|
85
|
+
|
86
|
+
BBAND = "BBAND" # composite, only for chart
|
87
|
+
UPPER_BBAND = "UPPER_BBAND"
|
88
|
+
LOWER_BBAND = "LOWER_BBAND"
|
89
|
+
WILLIAM_R = "WILLIAM_R"
|
90
|
+
ROC = "ROC"
|
91
|
+
CCI = "CCI"
|
92
|
+
|
93
|
+
CMO = "CMO"
|
94
|
+
CMO_SMOOTHED = "CMO_SMOOTHED"
|
95
|
+
MEDIAN_PRICE = "MEDIAN_PRICE"
|
96
|
+
TYPICAL_PRICE = "TYPICAL_PRICE"
|
97
|
+
ULTIMATE_OSC = "ULTIMATE_OSC"
|
98
|
+
PPO = "PPO"
|
99
|
+
|
100
|
+
STOCHASTICS = "STOCHASTICS" # composite only for charts
|
101
|
+
FAST_STOCHASTIC_OSC = "FAST_STOCHASTIC_OSC"
|
102
|
+
SLOW_STOCHASTIC_OSC = "SLOW_STOCHASTIC_OSC"
|
103
|
+
|
104
|
+
MAX = "MAX"
|
105
|
+
MIN = "MIN"
|
106
|
+
|
107
|
+
DOJI = "DOJI"
|
108
|
+
HAMMER = "HAMMER"
|
109
|
+
|
110
|
+
INVERTED_HAMMER = "INVERTED_HAMMER"
|
111
|
+
DRAGONFLY_DOJI = "DRAGONFLY_DOJI"
|
112
|
+
GRAVESTONE_DOJI = "GRAVESTONE_DOJI"
|
113
|
+
HANGING_MAN = "HANGING_MAN"
|
114
|
+
|
115
|
+
BULLISH = "BULLISH"
|
116
|
+
BEARISH = "BEARISH"
|
117
|
+
|
118
|
+
SUPPORT = "SUPPORT"
|
119
|
+
RESISTANCE = "RESISTANCE"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
import pytz
|
3
|
+
|
4
|
+
|
5
|
+
class ModelUtils:
|
6
|
+
|
7
|
+
# In Java, using Date includes timezone offset in JSON whereas using LocalDateTime does not
|
8
|
+
|
9
|
+
est_tz = pytz.timezone('America/New_York')
|
10
|
+
|
11
|
+
@staticmethod
|
12
|
+
def localizeDateTime(dt: datetime) -> datetime:
|
13
|
+
return ModelUtils.est_tz.localize(dt)
|
14
|
+
|
15
|
+
|
16
|
+
@staticmethod
|
17
|
+
def parseDatetime(date_str: str) -> datetime:
|
18
|
+
dt = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S.%f')
|
19
|
+
dt = dt.astimezone(ModelUtils.est_tz)
|
20
|
+
return dt
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def formatDatetime(dt: datetime) -> str:
|
24
|
+
return dt.strftime('%Y-%m-%dT%H:%M:%S.%f')
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def parseZonedDatetime(date_str: str) -> datetime:
|
28
|
+
dt = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S.%f%z')
|
29
|
+
dt = dt.astimezone(ModelUtils.est_tz)
|
30
|
+
return dt
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def formatZonedDatetime(dt: datetime) -> str:
|
34
|
+
return dt.strftime('%Y-%m-%dT%H:%M:%S.%f%z')
|
@@ -0,0 +1,270 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from datetime import datetime
|
5
|
+
from enum import Enum
|
6
|
+
from typing import List, Dict, Any, cast
|
7
|
+
|
8
|
+
from investfly.models.CommonModels import DatedValue
|
9
|
+
from investfly.models.MarketData import Security
|
10
|
+
from investfly.models.ModelUtils import ModelUtils
|
11
|
+
|
12
|
+
class PositionType(str, Enum):
|
13
|
+
LONG = "LONG",
|
14
|
+
SHORT = "SHORT"
|
15
|
+
CLOSE = "CLOSE"
|
16
|
+
|
17
|
+
def __str__(self):
|
18
|
+
return self.value
|
19
|
+
|
20
|
+
def __repr__(self):
|
21
|
+
return self.value
|
22
|
+
|
23
|
+
|
24
|
+
class TradeType(str, Enum):
|
25
|
+
BUY = "BUY"
|
26
|
+
SELL = "SELL"
|
27
|
+
SHORT = "SHORT"
|
28
|
+
COVER = "COVER"
|
29
|
+
|
30
|
+
def __str__(self):
|
31
|
+
return self.value
|
32
|
+
|
33
|
+
def __repr__(self):
|
34
|
+
return self.value
|
35
|
+
|
36
|
+
|
37
|
+
class OrderType(str, Enum):
|
38
|
+
MARKET_ORDER = "MARKET_ORDER"
|
39
|
+
LIMIT_ORDER = "LIMIT_ORDER"
|
40
|
+
STOP_ORDER = "STOP_ORDER"
|
41
|
+
ONE_CANCEL_OTHER = "ONE_CANCEL_OTHER"
|
42
|
+
CUSTOM_CONDITION = "CUSTOM_CONDITION"
|
43
|
+
|
44
|
+
|
45
|
+
class Broker(str, Enum):
|
46
|
+
INVESTFLY = "INVESTFLY"
|
47
|
+
TRADIER = "TRADIER"
|
48
|
+
TDAMERITRADE = "TDAMERITRADE"
|
49
|
+
BACKTEST = "BACKTEST"
|
50
|
+
|
51
|
+
def __str__(self):
|
52
|
+
return self.value
|
53
|
+
|
54
|
+
def __repr__(self):
|
55
|
+
return self.value
|
56
|
+
|
57
|
+
|
58
|
+
@dataclass
|
59
|
+
class TradeOrder:
|
60
|
+
security: Security
|
61
|
+
tradeType: TradeType
|
62
|
+
orderType: OrderType = OrderType.MARKET_ORDER
|
63
|
+
quantity: int | None = None
|
64
|
+
maxAmount: float | None = None
|
65
|
+
limitPrice: float | None = None # If left empty, will use latest quote as limit price
|
66
|
+
|
67
|
+
def toDict(self) -> Dict[str, Any]:
|
68
|
+
jsonDict = self.__dict__.copy()
|
69
|
+
jsonDict["security"] = self.security.toDict()
|
70
|
+
return jsonDict
|
71
|
+
|
72
|
+
|
73
|
+
@dataclass
|
74
|
+
class OrderStatus:
|
75
|
+
orderId: int
|
76
|
+
status: str
|
77
|
+
message: str | None = None
|
78
|
+
|
79
|
+
@staticmethod
|
80
|
+
def fromDict(jsonDict: Dict[str, Any]) -> OrderStatus:
|
81
|
+
status = OrderStatus(jsonDict["orderId"], jsonDict["status"])
|
82
|
+
status.message = jsonDict.get("message")
|
83
|
+
return status
|
84
|
+
|
85
|
+
|
86
|
+
@dataclass
|
87
|
+
class PendingOrder(TradeOrder):
|
88
|
+
orderId: str | None = None
|
89
|
+
status: str | None = None
|
90
|
+
scheduledDate: datetime | None = None
|
91
|
+
|
92
|
+
@staticmethod
|
93
|
+
def fromDict(jsonDict: Dict[str, Any]) -> Any:
|
94
|
+
security = Security.fromDict(jsonDict["security"])
|
95
|
+
pendingOrder = PendingOrder(security, TradeType(jsonDict["tradeType"]))
|
96
|
+
pendingOrder.quantity = jsonDict.get("quantity")
|
97
|
+
pendingOrder.maxAmount = jsonDict.get("maxAmount")
|
98
|
+
pendingOrder.orderId = jsonDict.get("orderId")
|
99
|
+
pendingOrder.status = jsonDict.get("status")
|
100
|
+
pendingOrder.scheduledDate = ModelUtils.parseZonedDatetime(cast(str, jsonDict.get("scheduledDate")))
|
101
|
+
return pendingOrder
|
102
|
+
|
103
|
+
|
104
|
+
@dataclass()
|
105
|
+
class Balances:
|
106
|
+
buyingPower: float
|
107
|
+
cashBalance: float
|
108
|
+
currentValue: float
|
109
|
+
initialAmount: float | None = None
|
110
|
+
|
111
|
+
@staticmethod
|
112
|
+
def fromDict(jsonDict: Dict[str, Any]) -> Balances:
|
113
|
+
balances = Balances(jsonDict["buyingPower"],
|
114
|
+
jsonDict["cashBalance"],
|
115
|
+
jsonDict["currentValue"])
|
116
|
+
balances.initialAmount = jsonDict.get("initialAmount")
|
117
|
+
return balances
|
118
|
+
|
119
|
+
|
120
|
+
@dataclass
|
121
|
+
class CompletedTrade:
|
122
|
+
security: Security
|
123
|
+
date: datetime
|
124
|
+
price: float
|
125
|
+
quantity: int
|
126
|
+
tradeType: TradeType
|
127
|
+
|
128
|
+
@staticmethod
|
129
|
+
def fromDict(jsonDict: Dict[str, Any]) -> CompletedTrade:
|
130
|
+
security = Security.fromDict(jsonDict["security"])
|
131
|
+
date = ModelUtils.parseZonedDatetime(jsonDict["date"])
|
132
|
+
return CompletedTrade(security, date, jsonDict["price"], jsonDict["quantity"], TradeType[jsonDict["tradeType"]])
|
133
|
+
|
134
|
+
|
135
|
+
@dataclass
|
136
|
+
class ClosedPosition:
|
137
|
+
security: Security
|
138
|
+
position: PositionType
|
139
|
+
openDate: datetime
|
140
|
+
closeDate: datetime
|
141
|
+
openPrice: float
|
142
|
+
closePrice: float
|
143
|
+
quantity: int
|
144
|
+
profitLoss: float|None = None
|
145
|
+
percentChange: float| None = None
|
146
|
+
|
147
|
+
@staticmethod
|
148
|
+
def fromDict(jsonDict: Dict[str, Any]) -> ClosedPosition:
|
149
|
+
security = Security.fromDict(jsonDict['security'])
|
150
|
+
position = PositionType[jsonDict['position']]
|
151
|
+
openDate = ModelUtils.parseZonedDatetime(jsonDict['openDate'])
|
152
|
+
closeDate = ModelUtils.parseZonedDatetime((jsonDict['closeDate']))
|
153
|
+
openPrice = jsonDict['openPrice']
|
154
|
+
closePrice = jsonDict['closePrice']
|
155
|
+
quantity = jsonDict['quantity']
|
156
|
+
|
157
|
+
closedPosition = ClosedPosition(security, position, openDate, closeDate, openPrice, closePrice, quantity)
|
158
|
+
if "profitLoss" in jsonDict:
|
159
|
+
closedPosition.profitLoss = jsonDict['profitLoss']
|
160
|
+
if "percentChange" in jsonDict:
|
161
|
+
closedPosition.percentChange = jsonDict['percentChange']
|
162
|
+
|
163
|
+
return closedPosition
|
164
|
+
|
165
|
+
def toDict(self) -> Dict[str, Any]:
|
166
|
+
jsonDict = self.__dict__.copy()
|
167
|
+
jsonDict["security"] = self.security.toDict()
|
168
|
+
jsonDict["position"] = self.position.value
|
169
|
+
jsonDict["openDate"] = ModelUtils.formatZonedDatetime(self.openDate)
|
170
|
+
jsonDict["closeDate"] = ModelUtils.formatZonedDatetime((self.closeDate))
|
171
|
+
return jsonDict
|
172
|
+
|
173
|
+
@dataclass
|
174
|
+
class OpenPosition:
|
175
|
+
security: Security
|
176
|
+
position: PositionType
|
177
|
+
avgPrice: float
|
178
|
+
quantity: int
|
179
|
+
purchaseDate: datetime
|
180
|
+
currentPrice: float | None = None
|
181
|
+
currentValue: float | None = None
|
182
|
+
profitLoss: float | None = None
|
183
|
+
percentChange: float | None = None
|
184
|
+
|
185
|
+
@staticmethod
|
186
|
+
def fromDict(jsonDict: Dict[str, Any]) -> OpenPosition:
|
187
|
+
security = Security.fromDict(jsonDict["security"])
|
188
|
+
openPos = OpenPosition(security,
|
189
|
+
PositionType(jsonDict["position"]),
|
190
|
+
jsonDict["avgPrice"],
|
191
|
+
jsonDict["quantity"],
|
192
|
+
ModelUtils.parseZonedDatetime(jsonDict["purchaseDate"])
|
193
|
+
)
|
194
|
+
openPos.currentPrice = jsonDict.get("currentPrice")
|
195
|
+
openPos.currentValue = jsonDict.get("currentValue")
|
196
|
+
openPos.profitLoss = jsonDict.get("profitLoss")
|
197
|
+
openPos.percentChange = jsonDict.get("percentChange")
|
198
|
+
return openPos
|
199
|
+
|
200
|
+
def toDict(self) -> Dict[str, Any]:
|
201
|
+
jsonDict = self.__dict__.copy()
|
202
|
+
jsonDict["security"] = self.security.toDict()
|
203
|
+
jsonDict["purchaseDate"] = ModelUtils.formatZonedDatetime(self.purchaseDate)
|
204
|
+
return jsonDict
|
205
|
+
|
206
|
+
|
207
|
+
class Portfolio:
|
208
|
+
def __init__(self, portfolioId: str, broker: Broker, balances: Balances) -> None:
|
209
|
+
self.portfolioId = portfolioId
|
210
|
+
self.broker = broker
|
211
|
+
self.balances: Balances = balances
|
212
|
+
self.openPositions: List[OpenPosition] = []
|
213
|
+
self.pendingOrders: List[PendingOrder] = []
|
214
|
+
self.completedTrades: List[CompletedTrade] = []
|
215
|
+
|
216
|
+
def __str__(self) -> str:
|
217
|
+
json_dict = self.__dict__.copy()
|
218
|
+
json_dict["openPositions"] = len(self.openPositions)
|
219
|
+
json_dict["completedTrades"] = len(self.completedTrades)
|
220
|
+
json_dict["pendingOrders"] = len(self.pendingOrders)
|
221
|
+
return str(json_dict)
|
222
|
+
|
223
|
+
def __repr__(self):
|
224
|
+
return self.__str__()
|
225
|
+
|
226
|
+
|
227
|
+
@dataclass
|
228
|
+
class PortfolioPerformance:
|
229
|
+
netReturn: float|None = None
|
230
|
+
annualizedReturn: float|None = None
|
231
|
+
profitFactor: float|None = None
|
232
|
+
|
233
|
+
totalTrades: int | None = None
|
234
|
+
winRatioPct: float| None = None
|
235
|
+
avgProfitPerTradePct: float| None = None
|
236
|
+
avgLossPerTradePct: float| None = None
|
237
|
+
|
238
|
+
meanReturnPerTradePct: float | None = None
|
239
|
+
sharpeRatioPerTrade: float | None = None
|
240
|
+
|
241
|
+
maxDrawdownPct: float|None = None
|
242
|
+
portfolioValues: List[DatedValue]|None = None
|
243
|
+
|
244
|
+
def toDict(self) -> Dict[str, Any]:
|
245
|
+
jsonDict = self.__dict__.copy()
|
246
|
+
if self.portfolioValues is not None:
|
247
|
+
jsonDict["portfolioValues"] = [dv.toJsonDict() for dv in self.portfolioValues]
|
248
|
+
else:
|
249
|
+
jsonDict['portfolioValues'] = []
|
250
|
+
return jsonDict
|
251
|
+
|
252
|
+
@staticmethod
|
253
|
+
def fromDict(jsonDict: Dict[str, Any]) -> PortfolioPerformance:
|
254
|
+
perf = PortfolioPerformance()
|
255
|
+
for key, value in jsonDict.items():
|
256
|
+
setattr(perf, key, value)
|
257
|
+
if 'portfolioValues' in jsonDict:
|
258
|
+
perf.portfolioValues = [ DatedValue.fromDict(dv) for dv in jsonDict['portfolioValues']]
|
259
|
+
else:
|
260
|
+
perf.portfolioValues = []
|
261
|
+
return perf
|
262
|
+
|
263
|
+
def __str__(self):
|
264
|
+
jsonDict = self.__dict__.copy()
|
265
|
+
if 'portfolioValues' in jsonDict:
|
266
|
+
del jsonDict['portfolioValues']
|
267
|
+
return str(jsonDict)
|
268
|
+
|
269
|
+
def __repr__(self):
|
270
|
+
return self.__str__()
|