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,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__()