investing-algorithm-framework 6.9.1__py3-none-any.whl → 7.19.15__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.

Potentially problematic release.


This version of investing-algorithm-framework might be problematic. Click here for more details.

Files changed (192) hide show
  1. investing_algorithm_framework/__init__.py +147 -44
  2. investing_algorithm_framework/app/__init__.py +23 -6
  3. investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
  4. investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
  5. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  6. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  7. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  8. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  9. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  10. investing_algorithm_framework/app/app.py +1322 -707
  11. investing_algorithm_framework/app/context.py +196 -88
  12. investing_algorithm_framework/app/eventloop.py +590 -0
  13. investing_algorithm_framework/app/reporting/__init__.py +16 -5
  14. investing_algorithm_framework/app/reporting/ascii.py +57 -202
  15. investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
  16. investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
  17. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  18. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  19. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
  20. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  21. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  22. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
  23. investing_algorithm_framework/app/reporting/generate.py +100 -114
  24. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
  25. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
  26. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
  27. investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
  28. investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
  29. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
  30. investing_algorithm_framework/app/strategy.py +315 -175
  31. investing_algorithm_framework/app/task.py +5 -3
  32. investing_algorithm_framework/cli/cli.py +30 -12
  33. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
  34. investing_algorithm_framework/cli/initialize_app.py +20 -1
  35. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
  36. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  37. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  38. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
  39. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
  40. investing_algorithm_framework/create_app.py +3 -5
  41. investing_algorithm_framework/dependency_container.py +25 -39
  42. investing_algorithm_framework/domain/__init__.py +45 -38
  43. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  44. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  45. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  46. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  47. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  48. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  49. investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
  50. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  51. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  52. investing_algorithm_framework/domain/config.py +27 -0
  53. investing_algorithm_framework/domain/constants.py +6 -34
  54. investing_algorithm_framework/domain/data_provider.py +200 -56
  55. investing_algorithm_framework/domain/exceptions.py +34 -1
  56. investing_algorithm_framework/domain/models/__init__.py +10 -19
  57. investing_algorithm_framework/domain/models/base_model.py +0 -6
  58. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  59. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  60. investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
  61. investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
  62. investing_algorithm_framework/domain/models/order/order.py +34 -13
  63. investing_algorithm_framework/domain/models/order/order_status.py +1 -1
  64. investing_algorithm_framework/domain/models/order/order_type.py +1 -1
  65. investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
  66. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
  67. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
  68. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  69. investing_algorithm_framework/domain/models/position/position.py +9 -0
  70. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  71. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  72. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  73. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  74. investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
  75. investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
  76. investing_algorithm_framework/domain/models/time_frame.py +7 -0
  77. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  78. investing_algorithm_framework/domain/models/time_unit.py +63 -1
  79. investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
  80. investing_algorithm_framework/domain/models/trade/trade.py +56 -32
  81. investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
  82. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
  83. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
  84. investing_algorithm_framework/domain/order_executor.py +19 -0
  85. investing_algorithm_framework/domain/portfolio_provider.py +20 -1
  86. investing_algorithm_framework/domain/services/__init__.py +0 -13
  87. investing_algorithm_framework/domain/strategy.py +1 -29
  88. investing_algorithm_framework/domain/utils/__init__.py +5 -1
  89. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  90. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  91. investing_algorithm_framework/domain/utils/polars.py +17 -14
  92. investing_algorithm_framework/download_data.py +40 -10
  93. investing_algorithm_framework/infrastructure/__init__.py +13 -25
  94. investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
  95. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
  96. investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
  97. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  98. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  99. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
  100. investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
  101. investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
  102. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
  103. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
  104. investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
  105. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  106. investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
  107. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
  108. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
  109. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
  110. investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
  111. investing_algorithm_framework/services/__init__.py +105 -8
  112. investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
  113. investing_algorithm_framework/services/configuration_service.py +14 -4
  114. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  115. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  116. investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
  117. investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
  118. investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
  119. investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
  120. investing_algorithm_framework/services/metrics/generate.py +358 -0
  121. investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
  122. investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
  123. investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
  124. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  125. investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
  126. investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
  127. investing_algorithm_framework/services/metrics/trades.py +500 -0
  128. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  129. investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
  130. investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
  131. investing_algorithm_framework/services/order_service/order_service.py +9 -71
  132. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
  133. investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
  134. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
  135. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
  136. investing_algorithm_framework/services/repository_service.py +5 -2
  137. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  138. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
  139. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  140. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  141. investing_algorithm_framework/services/trade_service/__init__.py +7 -1
  142. investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
  143. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  144. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  145. investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
  146. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
  147. investing_algorithm_framework/app/reporting/evaluation.py +0 -243
  148. investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
  149. investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
  150. investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
  151. investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
  152. investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
  153. investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
  154. investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
  155. investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
  156. investing_algorithm_framework/domain/models/data_source.py +0 -21
  157. investing_algorithm_framework/domain/models/date_range.py +0 -64
  158. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
  159. investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
  160. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  161. investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
  162. investing_algorithm_framework/domain/services/market_service.py +0 -153
  163. investing_algorithm_framework/domain/services/observable.py +0 -51
  164. investing_algorithm_framework/domain/services/observer.py +0 -19
  165. investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
  166. investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
  167. investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
  168. investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
  169. investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
  170. investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
  171. investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
  172. investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
  173. investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
  174. investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
  175. investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
  176. investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
  177. investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
  178. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
  179. investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
  180. /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
  181. /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
  182. /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
  183. /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
  184. /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
  185. /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
  186. /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
  187. /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
  188. /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
  189. /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
  190. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
  191. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
  192. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
 
4
- class MarketDataType(Enum):
4
+ class DataType(Enum):
5
5
  OHLCV = "OHLCV"
6
6
  TICKER = "TICKER"
7
7
  ORDER_BOOK = "ORDER_BOOK"
@@ -12,24 +12,24 @@ class MarketDataType(Enum):
12
12
 
13
13
  if isinstance(value, str):
14
14
 
15
- for entry in MarketDataType:
15
+ for entry in DataType:
16
16
 
17
17
  if value.upper() == entry.value:
18
18
  return entry
19
19
 
20
20
  raise ValueError(
21
- f"Could not convert {value} to MarketDataType"
21
+ f"Could not convert {value} to DataType"
22
22
  )
23
23
 
24
24
  @staticmethod
25
25
  def from_value(value):
26
26
 
27
27
  if isinstance(value, str):
28
- return MarketDataType.from_string(value)
28
+ return DataType.from_string(value)
29
29
 
30
- if isinstance(value, MarketDataType):
30
+ if isinstance(value, DataType):
31
31
 
32
- for entry in MarketDataType:
32
+ for entry in DataType:
33
33
 
34
34
  if value == entry:
35
35
  return entry
@@ -43,4 +43,4 @@ class MarketDataType(Enum):
43
43
  if isinstance(other, Enum):
44
44
  return self.value == other.value
45
45
  else:
46
- return MarketDataType.from_string(other) == self
46
+ return DataType.from_string(other) == self
@@ -52,6 +52,12 @@ class MarketCredential:
52
52
  environment_variable = f"{self.market.upper()}_SECRET_KEY"
53
53
  self._secret_key = os.getenv(environment_variable)
54
54
 
55
+ if self.api_key is not None:
56
+ self._api_key = self.api_key.strip()
57
+
58
+ if self.secret_key is not None:
59
+ self._secret_key = self.secret_key.strip()
60
+
55
61
  def get_api_key(self):
56
62
  return self.api_key
57
63
 
@@ -25,7 +25,7 @@ class Order(BaseModel):
25
25
  order_type,
26
26
  order_side,
27
27
  amount,
28
- status=OrderStatus.CREATED.value,
28
+ status: OrderStatus | None | str = OrderStatus.CREATED.value,
29
29
  target_symbol=None,
30
30
  trading_symbol=None,
31
31
  price=None,
@@ -40,7 +40,8 @@ class Order(BaseModel):
40
40
  order_fee=None,
41
41
  order_fee_currency=None,
42
42
  order_fee_rate=None,
43
- id=None
43
+ id=None,
44
+ metadata=None,
44
45
  ):
45
46
  if target_symbol is None:
46
47
  raise OperationalException("Target symbol is not specified")
@@ -85,6 +86,7 @@ class Order(BaseModel):
85
86
  self.order_fee_rate = order_fee_rate
86
87
  self.id = id
87
88
  self.cost = cost
89
+ self.metadata = metadata if metadata is not None else {}
88
90
 
89
91
  def get_id(self):
90
92
  return self.id
@@ -191,6 +193,11 @@ class Order(BaseModel):
191
193
  return (self.get_target_symbol().upper() + "/"
192
194
  + self.get_trading_symbol().upper())
193
195
 
196
+ @property
197
+ def symbol(self):
198
+ return (self.get_target_symbol().upper() + "/"
199
+ + self.get_trading_symbol().upper())
200
+
194
201
  def get_available_amount(self):
195
202
 
196
203
  if self._available_amount is None:
@@ -222,14 +229,18 @@ class Order(BaseModel):
222
229
  dict: A dictionary representation of the Order object.
223
230
  """
224
231
 
225
- if datetime_format is not None:
226
- created_at = self.created_at.strftime(datetime_format) \
227
- if self.created_at else None
228
- updated_at = self.updated_at.strftime(datetime_format) \
229
- if self.updated_at else None
230
- else:
231
- created_at = self.created_at
232
- updated_at = self.updated_at
232
+ def ensure_iso(value):
233
+ if hasattr(value, "isoformat"):
234
+ if value.tzinfo is None:
235
+ value = value.replace(tzinfo=timezone.utc)
236
+ return value.isoformat()
237
+ return value
238
+
239
+ created_at = ensure_iso(self.created_at) if self.created_at else None
240
+ updated_at = ensure_iso(self.updated_at) if self.updated_at else None
241
+
242
+ # Ensure status is a string
243
+ self.status = OrderStatus.from_value(self.status).value
233
244
 
234
245
  return {
235
246
  "id": self.id,
@@ -249,6 +260,7 @@ class Order(BaseModel):
249
260
  "order_fee_currency": self.order_fee_currency,
250
261
  "order_fee_rate": self.order_fee_rate,
251
262
  "order_fee": self.order_fee,
263
+ "metadata": self.metadata if hasattr(self, 'metadata') else {},
252
264
  }
253
265
 
254
266
  @staticmethod
@@ -271,7 +283,8 @@ class Order(BaseModel):
271
283
  updated_at = parse(updated_at)
272
284
 
273
285
  order = Order(
274
- external_id=data.get("id", None),
286
+ id=data.get("id", None),
287
+ external_id=data.get("external_id", None),
275
288
  target_symbol=target_symbol,
276
289
  trading_symbol=trading_symbol,
277
290
  price=data.get("price", None),
@@ -282,12 +295,13 @@ class Order(BaseModel):
282
295
  filled=data.get("filled", None),
283
296
  remaining=data.get("remaining", None),
284
297
  fee=data.get("fee", None),
298
+ cost=data.get("cost", None),
285
299
  created_at=created_at,
286
300
  updated_at=updated_at,
287
301
  order_fee=data.get("order_fee", None),
288
302
  order_fee_currency=data.get("order_fee_currency", None),
289
303
  order_fee_rate=data.get("order_fee_rate", None),
290
- id=data.get("id", None)
304
+ metadata=data.get("metadata", {}),
291
305
  )
292
306
  return order
293
307
 
@@ -311,6 +325,13 @@ class Order(BaseModel):
311
325
  order_fee_currency = ccxt_fee.get("currency", None)
312
326
  order_fee_rate = ccxt_fee.get("rate", None)
313
327
 
328
+ created_at = ccxt_order.get("datetime", None)
329
+ created_at = parse(created_at) if created_at else None
330
+
331
+ # Make sure that the created_at is a utc datetime object
332
+ if created_at and created_at.tzinfo is None:
333
+ created_at = created_at.replace(tzinfo=timezone.utc)
334
+
314
335
  return Order(
315
336
  external_id=ccxt_order.get("id", None),
316
337
  target_symbol=target_symbol,
@@ -326,7 +347,7 @@ class Order(BaseModel):
326
347
  order_fee=order_fee,
327
348
  order_fee_currency=order_fee_currency,
328
349
  order_fee_rate=order_fee_rate,
329
- created_at=parse(ccxt_order.get("datetime", None))
350
+ created_at=created_at
330
351
  )
331
352
 
332
353
  def __repr__(self):
@@ -31,7 +31,7 @@ class OrderStatus(Enum):
31
31
  elif isinstance(value, str):
32
32
  return OrderStatus.from_string(value)
33
33
 
34
- raise ValueError("Could not convert value to OrderStatus")
34
+ raise ValueError(f"Could not convert value: {value} to OrderStatus")
35
35
 
36
36
  def equals(self, other):
37
37
  return OrderStatus.from_value(other) == self
@@ -13,7 +13,7 @@ class OrderType(Enum):
13
13
  if value.upper() == order_type.value:
14
14
  return order_type
15
15
 
16
- raise ValueError("Could not convert value to OrderType")
16
+ raise ValueError(f"Could not convert value: {value} to OrderType")
17
17
 
18
18
  @staticmethod
19
19
  def from_value(value):
@@ -40,6 +40,7 @@ class Portfolio(BaseModel):
40
40
  trading_symbol,
41
41
  net_size,
42
42
  unallocated,
43
+ initial_balance,
43
44
  market,
44
45
  realized=0,
45
46
  total_revenue=0,
@@ -49,7 +50,6 @@ class Portfolio(BaseModel):
49
50
  created_at=None,
50
51
  updated_at=None,
51
52
  initialized=False,
52
- initial_balance=None
53
53
  ):
54
54
  self.identifier = identifier
55
55
  self.updated_at = None
@@ -66,6 +66,7 @@ class Portfolio(BaseModel):
66
66
  self.created_at = created_at
67
67
  self.updated_at = updated_at
68
68
  self.initialized = initialized
69
+ self._allocated = None
69
70
 
70
71
  def __repr__(self):
71
72
  return self.repr(
@@ -119,6 +120,18 @@ class Portfolio(BaseModel):
119
120
  def get_initial_balance(self):
120
121
  return self.initial_balance
121
122
 
123
+ @property
124
+ def allocated(self):
125
+
126
+ if self._allocated is None:
127
+ return 0.0
128
+
129
+ return self._allocated
130
+
131
+ @allocated.setter
132
+ def allocated(self, value):
133
+ self._allocated = value
134
+
122
135
  @staticmethod
123
136
  def from_portfolio_configuration(portfolio_configuration):
124
137
  """
@@ -10,7 +10,7 @@ class PortfolioConfiguration(BaseModel):
10
10
  This class represents a portfolio configuration. It is used to
11
11
  configure the portfolio that the user wants to create.
12
12
 
13
- The portfolio configuration will have the following attributes:
13
+ Attributes:
14
14
  - market: The market where the portfolio will be created
15
15
  - trading_symbol: The trading symbol of the portfolio
16
16
  - track_from: The date from which the portfolio will be tracked
@@ -33,6 +33,10 @@ class PortfolioConfiguration(BaseModel):
33
33
  initial_balance=None,
34
34
  ):
35
35
  self._market = market
36
+
37
+ if self._market is not None:
38
+ self._market = self._market.upper()
39
+
36
40
  self._track_from = None
37
41
  self._trading_symbol = trading_symbol.upper()
38
42
  self._identifier = identifier
@@ -1,4 +1,7 @@
1
+ from datetime import timezone
2
+
1
3
  from dateutil import parser
4
+
2
5
  from investing_algorithm_framework.domain.models.base_model import BaseModel
3
6
 
4
7
 
@@ -17,7 +20,8 @@ class PortfolioSnapshot(BaseModel):
17
20
  total_value=None,
18
21
  cash_flow=None,
19
22
  created_at=None,
20
- position_snapshots=None
23
+ position_snapshots=None,
24
+ metadata=None,
21
25
  ):
22
26
  self.portfolio_id = portfolio_id
23
27
  self.trading_symbol = trading_symbol
@@ -29,7 +33,15 @@ class PortfolioSnapshot(BaseModel):
29
33
  self.net_size = net_size
30
34
  self.total_cost = total_cost
31
35
  self.cash_flow = cash_flow
32
- self.created_at = created_at
36
+ self.metadata = metadata if metadata is not None else {}
37
+
38
+ if created_at is not None and isinstance(created_at, str):
39
+ self.created_at = parser.parse(created_at)
40
+ else:
41
+ self.created_at = created_at
42
+
43
+ # Make sure that created_at is a timezone aware datetime object
44
+ self.created_at = self.created_at.replace(tzinfo=timezone.utc)
33
45
 
34
46
  if position_snapshots is None:
35
47
  position_snapshots = []
@@ -120,6 +132,7 @@ class PortfolioSnapshot(BaseModel):
120
132
  total_revenue=self.total_revenue,
121
133
  total_cost=self.total_cost,
122
134
  cash_flow=self.cash_flow,
135
+ metadata=self.metadata,
123
136
  )
124
137
 
125
138
  def to_dict(self, datetime_format=None):
@@ -134,18 +147,30 @@ class PortfolioSnapshot(BaseModel):
134
147
  Returns:
135
148
  dict: A dictionary representation of the portfolio snapshot object.
136
149
  """
150
+ def ensure_iso(value):
151
+ if hasattr(value, "isoformat"):
152
+ if value.tzinfo is None:
153
+ value = value.replace(tzinfo=timezone.utc)
154
+ return value.isoformat()
155
+ return value
137
156
 
138
- if datetime_format is not None:
139
- created_at = self.created_at.strftime(datetime_format) \
140
- if self.created_at else None
141
-
142
- else:
143
- created_at = self.created_at
157
+ created_at = ensure_iso(self.created_at) if self.created_at else None
144
158
 
145
159
  return {
146
- "net_size": self.net_size,
147
- "created_at": created_at,
148
- "total_value": self.total_value,
160
+ "metadata": self.metadata if self.metadata else {},
161
+ "portfolio_id": self.portfolio_id if self.portfolio_id else "",
162
+ "trading_symbol": self.trading_symbol
163
+ if self.trading_symbol else "",
164
+ "pending_value": self.pending_value if self.pending_value else 0.0,
165
+ "unallocated": self.unallocated if self.unallocated else 0.0,
166
+ "total_net_gain": self.total_net_gain
167
+ if self.total_net_gain else 0.0,
168
+ "total_revenue": self.total_revenue if self.total_revenue else 0.0,
169
+ "total_cost": self.total_cost if self.total_cost else 0.0,
170
+ "cash_flow": self.cash_flow if self.cash_flow else 0.0,
171
+ "net_size": self.net_size if self.net_size else 0.0,
172
+ "created_at": created_at if created_at else "",
173
+ "total_value": self.total_value if self.total_value else 0.0,
149
174
  }
150
175
 
151
176
  @staticmethod
@@ -161,8 +186,23 @@ class PortfolioSnapshot(BaseModel):
161
186
  """
162
187
  created_at_str = data.get("created_at")
163
188
  created_at = parser.parse(created_at_str)
189
+
190
+ # Ensure created_at is timezone aware
191
+ created_at = created_at.replace(tzinfo=timezone.utc)
192
+
164
193
  return PortfolioSnapshot(
165
194
  net_size=data.get("net_size", 0.0),
166
195
  created_at=created_at,
167
196
  total_value=data.get("total_value", 0.0),
197
+ trading_symbol=data.get(
198
+ "trading_symbol", None
199
+ ),
200
+ portfolio_id=data.get("portfolio_id", None),
201
+ pending_value=data.get("pending_value", 0.0),
202
+ unallocated=data.get("unallocated", 0.0),
203
+ total_net_gain=data.get("total_net_gain", 0.0),
204
+ total_revenue=data.get("total_revenue", 0.0),
205
+ total_cost=data.get("total_cost", 0.0),
206
+ cash_flow=data.get("cash_flow", 0.0),
207
+ metadata=data.get("metadata", {})
168
208
  )
@@ -1,4 +1,5 @@
1
1
  from .position import Position
2
2
  from .position_snapshot import PositionSnapshot
3
+ from .position_size import PositionSize
3
4
 
4
- __all__ = ["Position", "PositionSnapshot"]
5
+ __all__ = ["Position", "PositionSnapshot", "PositionSize"]
@@ -50,6 +50,15 @@ class Position(BaseModel):
50
50
  "portfolio_id": self.portfolio_id,
51
51
  }
52
52
 
53
+ @staticmethod
54
+ def from_dict(data: dict):
55
+ return Position(
56
+ symbol=data.get("symbol"),
57
+ amount=data.get("amount", 0),
58
+ cost=data.get("cost", 0),
59
+ portfolio_id=data.get("portfolio_id"),
60
+ )
61
+
53
62
  def __repr__(self):
54
63
  return self.repr(
55
64
  symbol=self.symbol,
@@ -0,0 +1,41 @@
1
+ from typing import Optional
2
+ from investing_algorithm_framework.domain.exceptions import \
3
+ OperationalException
4
+
5
+
6
+ class PositionSize:
7
+ """
8
+ Defines how much capital to allocate to a specific symbol.
9
+ """
10
+
11
+ def __init__(
12
+ self,
13
+ symbol: str,
14
+ percentage_of_portfolio: Optional[float] = None,
15
+ fixed_amount: Optional[float] = None
16
+ ):
17
+ self.symbol = symbol
18
+ self.percentage_of_portfolio = percentage_of_portfolio
19
+ self.fixed_amount = fixed_amount
20
+
21
+ def get_size(self, portfolio, asset_price) -> float:
22
+ """
23
+ Calculate size in currency/units.
24
+ """
25
+ if self.fixed_amount is not None:
26
+ return self.fixed_amount
27
+ elif self.percentage_of_portfolio is not None:
28
+ total_value = portfolio.get_unallocated() + portfolio.allocated
29
+ return total_value * (self.percentage_of_portfolio / 100)
30
+ else:
31
+ raise OperationalException(
32
+ "A position size object must have either a fixed amount or a "
33
+ "percentage of the portfolio defined."
34
+ )
35
+
36
+ def __repr__(self) -> str:
37
+ return (
38
+ f"PositionSize(symbol={self.symbol}, "
39
+ f"percentage_of_portfolio={self.percentage_of_portfolio}, "
40
+ f"fixed_amount={self.fixed_amount})"
41
+ )
@@ -0,0 +1,7 @@
1
+ from .stop_loss_rule import StopLossRule
2
+ from .take_profit_rule import TakeProfitRule
3
+
4
+ __all__ = [
5
+ "StopLossRule",
6
+ "TakeProfitRule",
7
+ ]
@@ -0,0 +1,51 @@
1
+ class StopLossRule:
2
+ """
3
+ A rule that defines when to trigger a stop loss based
4
+ on a specified threshold such as a percentage drop in price or
5
+ a fixed amount.
6
+
7
+ if trade_risk_type is fixed, the stop loss price is calculated as follows:
8
+ You buy an asset at $100.
9
+ You set a 5% stop loss, meaning you will sell if
10
+ the price drops to $95.
11
+ If the price rises to $120, the stop loss is not triggered.
12
+ But if the price keeps falling to $95, the stop loss triggers,
13
+ and you exit with a $5 loss.
14
+
15
+ if trade_risk_type is trailing, the stop loss price is
16
+ calculated as follows:
17
+ You buy an asset at $100.
18
+ You set a 5% trailing stop loss, meaning you will sell if
19
+ the price drops 5% from its peak at $96
20
+ If the price rises to $120, the stop loss adjusts
21
+ to $114 (5% below $120).
22
+ If the price falls to $114, the position is
23
+ closed, securing a $14 profit.
24
+ But if the price keeps rising to $150, the stop
25
+ loss moves up to $142.50.
26
+ If the price drops from $150 to $142.50, the stop
27
+ loss triggers, and you exit with a $42.50 profit.
28
+
29
+ Attributes:
30
+ - percentage_threshold (float): The percentage drop in price
31
+ that triggers the stop loss.
32
+ - trailing (bool): Indicates whether the stop loss is trailing
33
+ or fixed.
34
+ - sell_percentage (float): The percentage of the position to sell
35
+ when the stop loss is triggered.
36
+ - symbol (str): The symbol of the asset the stop loss rule
37
+ applies to. Symbol is defined as the target symbol
38
+ (the asset being traded) combined with the trading symbol
39
+ (the asset used to trade the target symbol), e.g., 'BTC-EUR'.
40
+ """
41
+ def __init__(
42
+ self,
43
+ percentage_threshold: float,
44
+ sell_percentage: float,
45
+ symbol: str,
46
+ trailing: bool = False,
47
+ ):
48
+ self.percentage_threshold = percentage_threshold
49
+ self.trailing = trailing
50
+ self.sell_percentage = sell_percentage
51
+ self.symbol = symbol
@@ -0,0 +1,55 @@
1
+ class TakeProfitRule:
2
+ """
3
+ A rule that defines when to trigger a take profit based
4
+ on a specified threshold such as a percentage gain in price or
5
+ a fixed amount.
6
+
7
+ if trailing is set to true, the take profit price is
8
+ calculated as follows:
9
+ You buy an asset at $100.
10
+ You set a 5% take profit, meaning you will sell if the price
11
+ rises to $105.
12
+ If the price rises to $120, the take profit triggers,
13
+ and you exit with a $20 profit.
14
+ But if the price keeps falling below $105, the take profit is not
15
+ triggered.
16
+
17
+ if trailing is set to true, the take profit price is
18
+ calculated as follows:
19
+ You buy an asset at $100.
20
+ You set a 5% trailing take profit, the moment the price rises
21
+ 5% the initial take profit mark will be set. This means you
22
+ will set the take_profit_price initially at none and
23
+ only if the price hits $105, you will set the
24
+ take_profit_price to $105.
25
+ if the price drops below $105, the take profit is triggered.
26
+ If the price rises to $120, the take profit adjusts to
27
+ $114 (5% below $120).
28
+ If the price falls to $114, the position is closed,
29
+ securing a $14 profit.
30
+ But if the price keeps rising to $150, the take profit
31
+ moves up to $142.50.
32
+
33
+
34
+ Attributes:
35
+ - percentage_threshold (float): The percentage gain in price
36
+ that triggers the stop loss.
37
+ - trailing (bool): Indicates whether the take profit is trailing
38
+ or fixed.
39
+ - sell_percentage (float): The percentage of the position to sell
40
+ when the take profit is triggered.
41
+ - symbol (str): The symbol of the asset the take profit rule
42
+ applies to. Symbol is defined as only the target symbol.
43
+ So for example, 'BTC' in 'BTC-EUR' or 'META' in 'META-USD'.
44
+ """
45
+ def __init__(
46
+ self,
47
+ percentage_threshold: float,
48
+ sell_percentage: float,
49
+ symbol: str,
50
+ trailing: bool = False,
51
+ ):
52
+ self.percentage_threshold = percentage_threshold
53
+ self.trailing = trailing
54
+ self.sell_percentage = sell_percentage
55
+ self.symbol = symbol
@@ -2,7 +2,6 @@ from enum import Enum
2
2
 
3
3
 
4
4
  class SnapshotInterval(Enum):
5
- TRADE_CLOSE = "TRADE_CLOSE"
6
5
  STRATEGY_ITERATION = "STRATEGY_ITERATION"
7
6
  DAILY = "DAILY"
8
7