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,29 +1,15 @@
1
+ from dateutil.parser import parse
2
+ from datetime import timezone
3
+ from datetime import datetime
4
+
1
5
  from investing_algorithm_framework.domain.models.base_model import BaseModel
2
- from investing_algorithm_framework.domain.models.trade.trade_risk_type import \
3
- TradeRiskType
4
6
 
5
7
 
6
8
  class TradeStopLoss(BaseModel):
7
9
  """
8
10
  TradeStopLoss represents a stop loss strategy for a trade.
9
11
 
10
- Attributes:
11
- trade: Trade - the trade that the take profit is for
12
- take_profit: float - the take profit percentage
13
- trade_risk_type: TradeRiskType - the type of trade risk, either
14
- trailing or fixed
15
- percentage: float - the stop loss percentage
16
- sell_percentage: float - the percentage of the trade to sell when the
17
- take profit is hit. Default is 100% of the trade. If the
18
- take profit percentage is lower than 100% a check must
19
- be made that the combined sell percentage of all
20
- take profits is less or equal than 100%.
21
- sell_amount: float - the amount to sell when the stop loss triggers
22
- sold_amount: float - the amount that has been sold
23
- high_water_mark: float - the highest price of the trade
24
- stop_loss_price: float - the price at which the stop loss triggers
25
-
26
- if trade_risk_type is fixed, the stop loss price is calculated as follows:
12
+ if trailing is set to False, the stop loss price is calculated as follows:
27
13
  You buy a stock at $100.
28
14
  You set a 5% stop loss, meaning you will sell if
29
15
  the price drops to $95.
@@ -31,7 +17,7 @@ class TradeStopLoss(BaseModel):
31
17
  But if the price keeps falling to $95, the stop loss triggers,
32
18
  and you exit with a $5 loss.
33
19
 
34
- if trade_risk_type is trailing, the stop loss price is
20
+ if trailing is set to True, the stop loss price is
35
21
  calculated as follows:
36
22
  You buy a stock at $100.
37
23
  You set a 5% trailing stop loss, meaning you will sell if
@@ -44,31 +30,65 @@ class TradeStopLoss(BaseModel):
44
30
  loss moves up to $142.50.
45
31
  If the price drops from $150 to $142.50, the stop
46
32
  loss triggers, and you exit with a $42.50 profit.
33
+
34
+ Attributes:
35
+ - trade (Trade): the trade that the take profit is for
36
+ - trailing (bool): whether the stop loss is trailing or fixed
37
+ - percentage (float): the stop loss percentage
38
+ - sell_percentage (float): the percentage of the trade to sell when the
39
+ take profit is hit. Default is 100% of the trade. If the
40
+ take profit percentage is lower than 100% a check must
41
+ be made that the combined sell percentage of all
42
+ take profits is less or equal than 100%.
43
+ - open_price (float): the price at which the trade was opened
44
+ - high_water_mark_date (str): the date at which the high water mark
45
+ was reached
46
+ - active (bool): whether the stop loss is active
47
+ - sell_amount (float): the amount to sell when the stop loss triggers
48
+ - sold_amount (float): the amount that has been sold
49
+ - high_water_mark (float) the highest price of the trade
50
+ - stop_loss_price (float) the price at which the stop loss triggers
47
51
  """
48
52
 
49
53
  def __init__(
50
54
  self,
51
55
  trade_id: int,
52
- trade_risk_type: TradeRiskType,
53
56
  percentage: float,
54
57
  open_price: float,
58
+ trailing: bool = False,
55
59
  total_amount_trade: float = None,
56
60
  sell_percentage: float = 100,
57
61
  active: bool = True,
62
+ triggered: bool = False,
63
+ triggered_at: datetime = None,
58
64
  sell_prices: str = None,
59
65
  sell_dates: str = None,
60
66
  sell_amount: float = None,
67
+ high_water_mark: float = None,
61
68
  high_water_mark_date: str = None,
69
+ created_at: datetime = None,
70
+ updated_at: datetime = None
62
71
  ):
63
72
  self.trade_id = trade_id
64
- self.trade_risk_type = TradeRiskType.from_value(trade_risk_type).value
73
+ self.trailing = trailing
65
74
  self.percentage = percentage
75
+ self.triggered = triggered
76
+ self.triggered_at = triggered_at
66
77
  self.sell_percentage = sell_percentage
67
- self.high_water_mark = open_price
78
+ self.high_water_mark = high_water_mark
68
79
  self.high_water_mark_date = high_water_mark_date
69
80
  self.open_price = open_price
70
- self.stop_loss_price = self.high_water_mark * \
71
- (1 - (self.percentage / 100))
81
+ self.created_at = created_at
82
+ self.updated_at = updated_at
83
+
84
+ if high_water_mark is None:
85
+ self.high_water_mark = open_price
86
+ self.stop_loss_price = self.open_price * \
87
+ (1 - (self.percentage / 100))
88
+ self.high_water_mark_date = created_at
89
+ else:
90
+ self.stop_loss_price = high_water_mark * \
91
+ (1 - (self.percentage / 100))
72
92
 
73
93
  if sell_amount is not None:
74
94
  self.sell_amount = sell_amount
@@ -91,13 +111,17 @@ class TradeStopLoss(BaseModel):
91
111
  and the percentage of the take profit.
92
112
 
93
113
  Args:
94
- current_price: float - the last reported price of the trade
114
+ current_price (float): the last reported price of the trade
115
+ date (datetime): the date of the last reported price
116
+
117
+ Returns:
118
+ None
95
119
  """
96
120
 
97
121
  if not self.active or self.sold_amount == self.sell_amount:
98
122
  return
99
123
 
100
- if TradeRiskType.FIXED.equals(self.trade_risk_type):
124
+ if not self.trailing:
101
125
  # Check if the current price is less than the high water mark
102
126
  if current_price > self.high_water_mark:
103
127
  self.high_water_mark = current_price
@@ -127,8 +151,8 @@ class TradeStopLoss(BaseModel):
127
151
  if not self.active or self.sold_amount == self.sell_amount:
128
152
  return False
129
153
 
130
- if TradeRiskType.FIXED.equals(self.trade_risk_type):
131
- # Check if the current price is less than the high water mark
154
+ if not self.trailing:
155
+ # Check if the current price is less than the high watermark
132
156
  return current_price <= self.stop_loss_price
133
157
  else:
134
158
  # Check if the current price is less than the stop loss price
@@ -151,10 +175,6 @@ class TradeStopLoss(BaseModel):
151
175
  trade stop loss stays active. The client that uses the
152
176
  trade stop loss is responsible for setting the trade stop
153
177
  loss to inactive.
154
-
155
- Args:
156
- trade: Trade - the trade to calculate the sell amount for
157
-
158
178
  """
159
179
 
160
180
  if not self.active:
@@ -218,27 +238,62 @@ class TradeStopLoss(BaseModel):
218
238
  self.sell_dates = None
219
239
 
220
240
  def to_dict(self, datetime_format=None):
241
+ def ensure_iso(value):
242
+
243
+ if value is None:
244
+ return value
245
+
246
+ if hasattr(value, "isoformat"):
247
+ if value.tzinfo is None:
248
+ value = value.replace(tzinfo=timezone.utc)
249
+ return value.isoformat()
250
+ return value
251
+
221
252
  return {
222
253
  "trade_id": self.trade_id,
223
- "trade_risk_type": self.trade_risk_type,
254
+ "trailing": self.trailing,
224
255
  "percentage": self.percentage,
225
256
  "open_price": self.open_price,
226
257
  "sell_percentage": self.sell_percentage,
227
258
  "high_water_mark": self.high_water_mark,
259
+ "high_water_mark_date": self.high_water_mark_date,
260
+ "triggered": self.triggered,
261
+ "triggered_at": ensure_iso(getattr(self, "triggered_at", None)),
228
262
  "stop_loss_price": self.stop_loss_price,
229
263
  "sell_amount": self.sell_amount,
230
264
  "sold_amount": self.sold_amount,
231
265
  "active": self.active,
232
- "sell_prices": self.sell_prices
266
+ "sell_prices": self.sell_prices,
267
+ "created_at": ensure_iso(self.created_at),
268
+ "updated_at": ensure_iso(self.updated_at)
233
269
  }
234
270
 
235
271
  @staticmethod
236
272
  def from_dict(data: dict):
273
+ created_at = parse(data["created_at"]) \
274
+ if data.get("created_at") is not None else None
275
+ updated_at = parse(data["updated_at"]) \
276
+ if data.get("updated_at") is not None else None
277
+ triggered_at = parse(data["triggered_at"]) \
278
+ if data.get("triggered_at") is not None else None
279
+ high_water_mark_date = parse(data.get("high_water_mark_date")) \
280
+ if data.get("high_water_mark_date") is not None else None
281
+
282
+ # Make sure all the dates are timezone utc aware
283
+ if created_at and created_at.tzinfo is None:
284
+ created_at = created_at.replace(tzinfo=timezone.utc)
285
+ if updated_at and updated_at.tzinfo is None:
286
+ updated_at = updated_at.replace(tzinfo=timezone.utc)
287
+ if triggered_at and triggered_at.tzinfo is None:
288
+ triggered_at = triggered_at.replace(tzinfo=timezone.utc)
289
+ if high_water_mark_date and high_water_mark_date.tzinfo is None:
290
+ high_water_mark_date = high_water_mark_date.replace(
291
+ tzinfo=timezone.utc
292
+ )
293
+
237
294
  return TradeStopLoss(
238
295
  trade_id=data.get("trade_id"),
239
- trade_risk_type=TradeRiskType.from_string(
240
- data.get("trade_risk_type")
241
- ),
296
+ trailing=data.get("trailing"),
242
297
  percentage=data.get("percentage"),
243
298
  open_price=data.get("open_price"),
244
299
  total_amount_trade=data.get("sell_amount", 0) /
@@ -248,20 +303,30 @@ class TradeStopLoss(BaseModel):
248
303
  sell_prices=data.get("sell_prices"),
249
304
  sell_dates=data.get("sell_dates"),
250
305
  sell_amount=data.get("sell_amount"),
251
- high_water_mark_date=data.get("high_water_mark_date")
306
+ high_water_mark=data.get("high_water_mark"),
307
+ high_water_mark_date=high_water_mark_date,
308
+ triggered=data.get("triggered", False),
309
+ triggered_at=triggered_at,
310
+ created_at=created_at,
311
+ updated_at=updated_at
252
312
  )
253
313
 
254
314
  def __repr__(self):
255
315
  return self.repr(
256
316
  trade_id=self.trade_id,
257
- trade_risk_type=self.trade_risk_type,
317
+ trailing=self.trailing,
258
318
  percentage=self.percentage,
259
319
  sell_percentage=self.sell_percentage,
260
320
  high_water_mark=self.high_water_mark,
321
+ high_water_mark_date=self.high_water_mark_date,
261
322
  open_price=self.open_price,
262
323
  stop_loss_price=self.stop_loss_price,
263
324
  sell_amount=self.sell_amount,
264
325
  sold_amount=self.sold_amount,
265
326
  sell_prices=self.sell_prices,
266
- active=self.active
327
+ active=self.active,
328
+ triggered=self.triggered,
329
+ triggered_at=self.triggered_at,
330
+ created_at=self.created_at,
331
+ updated_at=self.updated_at
267
332
  )
@@ -1,25 +1,14 @@
1
+ from datetime import timezone, datetime
2
+ from dateutil.parser import parse
3
+
1
4
  from investing_algorithm_framework.domain.models.base_model import BaseModel
2
- from investing_algorithm_framework.domain.models.trade.trade_risk_type import \
3
- TradeRiskType
4
5
 
5
6
 
6
7
  class TradeTakeProfit(BaseModel):
7
8
  """
8
9
  TradeTakeProfit represents a take profit strategy for a trade.
9
10
 
10
- Attributes:
11
- trade: Trade - the trade that the take profit is for
12
- take_profit: float - the take profit percentage
13
- trade_risk_type: TradeRiskType - the type of trade risk, either
14
- trailing or fixed
15
- percentage: float - the take profit percentage
16
- sell_percentage: float - the percentage of the trade to sell when the
17
- take profit is hit. Default is 100% of the trade.
18
- If the take profit percentage is lower than 100% a check
19
- must be made that the combined sell percentage of
20
- all take profits is less or equal than 100%.
21
-
22
- if trade_risk_type is fixed, the take profit price is
11
+ if trailing is set to False, the take profit price is
23
12
  calculated as follows:
24
13
  You buy a stock at $100.
25
14
  You set a 5% take profit, meaning you will sell if the price
@@ -29,7 +18,7 @@ class TradeTakeProfit(BaseModel):
29
18
  But if the price keeps falling below $105, the take profit is not
30
19
  triggered.
31
20
 
32
- if trade_risk_type is trailing, the take profit price is
21
+ if trailing is set to True, the take profit price is
33
22
  calculated as follows:
34
23
  You buy a stock at $100.
35
24
  You set a 5% trailing take profit, the moment the price rises
@@ -44,31 +33,65 @@ class TradeTakeProfit(BaseModel):
44
33
  securing a $14 profit.
45
34
  But if the price keeps rising to $150, the take profit
46
35
  moves up to $142.50.
36
+
37
+ Attributes:
38
+ - trade (Trade): the trade that the take profit is for
39
+ - trailing (bool): whether the take profit is trailing or fixed
40
+ - percentage (float): the stop loss percentage
41
+ - sell_percentage (float): the percentage of the trade to sell when the
42
+ take profit is hit. Default is 100% of the trade. If the
43
+ take profit percentage is lower than 100% a check must
44
+ be made that the combined sell percentage of all
45
+ take profits is less or equal than 100%.
46
+ - open_price (float): the price at which the trade was opened
47
+ - take_profit_price (float): the price at which the take profit
48
+ triggers
49
+ - high_water_mark_date (str): the date at which the high water mark
50
+ was reached
51
+ - active (bool): whether the take profit is active
52
+ - triggered (bool): whether the take profit has been triggered
53
+ - sell_amount (float): the amount to sell when the stop loss triggers
54
+ - sold_amount (float): the amount that has been sold
55
+ - high_water_mark (float) the highest price of the trade
56
+ - stop_loss_price (float) the price at which the stop loss triggers
47
57
  """
48
58
 
49
59
  def __init__(
50
60
  self,
51
61
  trade_id: int,
52
- trade_risk_type: TradeRiskType,
53
62
  percentage: float,
54
63
  open_price: float,
64
+ trailing: bool = False,
55
65
  total_amount_trade: float = None,
56
66
  sell_percentage: float = 100,
57
67
  active: bool = True,
68
+ triggered: bool = False,
69
+ triggered_at: datetime = None,
58
70
  sell_prices: str = None,
59
71
  sell_dates: str = None,
60
72
  sell_amount: float = None,
73
+ high_water_mark: float = None,
61
74
  high_water_mark_date: str = None,
75
+ created_at: datetime = None,
76
+ updated_at: datetime = None
62
77
  ):
63
78
  self.trade_id = trade_id
64
- self.trade_risk_type = TradeRiskType.from_value(trade_risk_type).value
79
+ self.trailing = trailing
65
80
  self.percentage = percentage
66
81
  self.sell_percentage = sell_percentage
67
- self.high_water_mark = None
82
+ self.triggered = triggered
83
+ self.triggered_at = triggered_at
84
+ self.high_water_mark = high_water_mark
68
85
  self.high_water_mark_date = high_water_mark_date
69
86
  self.open_price = open_price
70
- self.take_profit_price = open_price * \
71
- (1 + (self.percentage / 100))
87
+ self.created_at = created_at
88
+ self.updated_at = updated_at
89
+
90
+ if high_water_mark is None and not self.trailing:
91
+ self.take_profit_price = self.open_price * \
92
+ (1 + (self.percentage / 100))
93
+ else:
94
+ self.take_profit_price = None
72
95
 
73
96
  if sell_amount is not None:
74
97
  self.sell_amount = sell_amount
@@ -84,100 +107,99 @@ class TradeTakeProfit(BaseModel):
84
107
  """
85
108
  Function to update the take profit price based on
86
109
  the last reported price.
87
- The take profit price is only updated when the
88
- trade risk type is trailing.
89
- The take profit price is updated based on the
90
- current price and the percentage of the take profit.
110
+ For fixed take profits: track the high water mark when price
111
+ exceeds the take profit price.
112
+ For trailing take profits: update the take profit price based on
113
+ the current price and the percentage of the take profit.
91
114
 
92
115
  Args:
93
116
  current_price: float - the last reported price of the trade
117
+ date: the date of the price update
94
118
  """
95
119
 
96
- # Do nothing for fixed take profit
97
- if TradeRiskType.FIXED.equals(self.trade_risk_type):
98
-
99
- if self.high_water_mark is not None:
100
- if current_price > self.high_water_mark:
101
- self.high_water_mark = current_price
102
- self.high_water_mark_date = date
103
- else:
104
- if current_price >= self.take_profit_price:
120
+ if not self.trailing:
121
+ # Fixed take profit: track high watermark
122
+ if current_price >= self.take_profit_price:
123
+ if (self.high_water_mark is None
124
+ or current_price > self.high_water_mark):
105
125
  self.high_water_mark = current_price
106
126
  self.high_water_mark_date = date
107
- return
108
-
109
127
  return
110
- else:
111
128
 
112
- if self.high_water_mark is None:
113
-
114
- if current_price >= self.take_profit_price:
115
- self.high_water_mark = current_price
116
- self.high_water_mark_date = date
117
- new_take_profit_price = self.high_water_mark * \
118
- (1 - (self.percentage / 100))
129
+ # Trailing take profit logic
130
+ if self.high_water_mark is None:
131
+ # High water mark not set yet
132
+ # Calculate the initial take profit threshold
133
+ initial_threshold = self.open_price * (1 + (self.percentage / 100))
119
134
 
120
- if self.take_profit_price <= new_take_profit_price:
121
- self.take_profit_price = new_take_profit_price
122
-
123
- return
124
-
125
- # Check if the current price is less than the take profit price
126
- if current_price < self.take_profit_price:
127
- return
128
-
129
- # Increase the high water mark and take profit price
130
- elif current_price > self.high_water_mark:
135
+ # Wait for price to reach the initial take profit threshold
136
+ if current_price >= initial_threshold:
137
+ # Initial threshold reached, set high watermark
138
+ self.high_water_mark = current_price
139
+ self.high_water_mark_date = date
140
+ # Calculate new take profit price based on high watermark
141
+ self.take_profit_price = self.high_water_mark * \
142
+ (1 - (self.percentage / 100))
143
+ else:
144
+ # High watermark is set, check for updates
145
+ # Check if price has risen above high watermark (adjust upward)
146
+ if current_price > self.high_water_mark:
131
147
  self.high_water_mark = current_price
132
148
  self.high_water_mark_date = date
149
+ # Recalculate take profit price based on new high water mark
133
150
  new_take_profit_price = self.high_water_mark * \
134
151
  (1 - (self.percentage / 100))
135
-
136
- # Only increase the take profit price if the new take
137
- # profit price based on the new high water mark is higher
138
- # then the current take profit price
139
- if self.take_profit_price <= new_take_profit_price:
152
+ # Update take profit price if it's higher than current
153
+ if new_take_profit_price > self.take_profit_price:
140
154
  self.take_profit_price = new_take_profit_price
141
155
 
142
- return
143
-
144
156
  def has_triggered(self, current_price: float = None) -> bool:
145
157
 
146
- if TradeRiskType.FIXED.equals(self.trade_risk_type):
147
- # Check if the current price is less than the high water mark
158
+ if not self.trailing:
159
+ # Fixed take profit: trigger when price reaches take_profit_price
148
160
  return current_price >= self.take_profit_price
149
161
  else:
150
- # Always return false, when the high water mark is not set
151
- # But check if we can set the high water mark
162
+ # Trailing take profit logic
152
163
  if self.high_water_mark is None:
164
+ # High water mark not set yet
165
+ # Calculate the initial take profit threshold
166
+ # (open_price * (1 + percentage))
167
+ initial_threshold = (self.open_price
168
+ * (1 + (self.percentage / 100)))
169
+
170
+ # Wait for price to reach the initial take profit threshold
171
+ if current_price >= initial_threshold:
172
+ # Initial threshold reached, set high water mark
173
+ self.high_water_mark = current_price
174
+ # Calculate new take profit price based on high water mark
175
+ # This is the pullback level
176
+ # (high_water_mark * (1 - percentage))
177
+ self.take_profit_price = self.high_water_mark * \
178
+ (1 - (self.percentage / 100))
179
+ # Don't trigger yet, wait for pullback
180
+ return False
181
+ else:
182
+ # High watermark is set, check for triggers and updates
183
+
184
+ # Check if price has pulled back below take profit
185
+ # price (trigger condition)
186
+ if current_price < self.take_profit_price:
187
+ return True
153
188
 
154
- if current_price >= self.take_profit_price:
189
+ # Check if price has risen above high
190
+ # water mark (adjust upward)
191
+ if current_price > self.high_water_mark:
155
192
  self.high_water_mark = current_price
193
+ # Recalculate take profit price based on
194
+ # new high water mark
156
195
  new_take_profit_price = self.high_water_mark * \
157
196
  (1 - (self.percentage / 100))
158
- if self.take_profit_price <= new_take_profit_price:
197
+ # Update take profit price if it's higher than current
198
+ if new_take_profit_price > self.take_profit_price:
159
199
  self.take_profit_price = new_take_profit_price
160
200
 
161
201
  return False
162
202
 
163
- # Check if the current price is less than the take profit price
164
- if current_price < self.take_profit_price:
165
- return True
166
-
167
- # Increase the high watermark and take profit price
168
- elif current_price > self.high_water_mark:
169
- self.high_water_mark = current_price
170
- new_take_profit_price = self.high_water_mark * \
171
- (1 - (self.percentage / 100))
172
-
173
- # Only increase the take profit price if the new take
174
- # profit price based on the new high water mark is higher
175
- # then the current take profit price
176
- if self.take_profit_price <= new_take_profit_price:
177
- self.take_profit_price = new_take_profit_price
178
-
179
- return False
180
-
181
203
  def get_sell_amount(self) -> float:
182
204
  """
183
205
  Function to calculate the amount to sell based on the
@@ -189,9 +211,8 @@ class TradeTakeProfit(BaseModel):
189
211
  trade stop loss is responsible for setting the trade stop
190
212
  loss to inactive.
191
213
 
192
- Args:
193
- trade: Trade - the trade to calculate the sell amount for
194
-
214
+ Returns:
215
+ float - the amount to sell
195
216
  """
196
217
 
197
218
  if not self.active:
@@ -206,8 +227,8 @@ class TradeTakeProfit(BaseModel):
206
227
  date is added to the list of sell dates.
207
228
 
208
229
  Args:
209
- price: float - the price at which the trade was sold
210
- date: str - the date at which the trade was sold
230
+ price (float): the price at which the trade was sold
231
+ date (datetime): the date at which the trade was sold
211
232
 
212
233
  Returns:
213
234
  None
@@ -255,9 +276,16 @@ class TradeTakeProfit(BaseModel):
255
276
  self.sell_dates = None
256
277
 
257
278
  def to_dict(self, datetime_format=None):
279
+ def ensure_iso(value):
280
+ if hasattr(value, "isoformat"):
281
+ if value.tzinfo is None:
282
+ value = value.replace(tzinfo=timezone.utc)
283
+ return value.isoformat()
284
+ return value
285
+
258
286
  return {
259
287
  "trade_id": self.trade_id,
260
- "trade_risk_type": self.trade_risk_type,
288
+ "trailing": self.trailing,
261
289
  "percentage": self.percentage,
262
290
  "open_price": self.open_price,
263
291
  "sell_percentage": self.sell_percentage,
@@ -266,38 +294,72 @@ class TradeTakeProfit(BaseModel):
266
294
  "sell_amount": self.sell_amount,
267
295
  "sold_amount": self.sold_amount,
268
296
  "active": self.active,
269
- "sell_prices": self.sell_prices
297
+ "triggered": self.triggered,
298
+ "triggered_at": ensure_iso(self.triggered_at),
299
+ "high_water_mark_date": self.high_water_mark_date,
300
+ "sell_prices": self.sell_prices,
301
+ "created_at": ensure_iso(self.created_at),
302
+ "updated_at": ensure_iso(self.updated_at)
270
303
  }
271
304
 
272
305
  @staticmethod
273
306
  def from_dict(data: dict):
307
+ created_at = parse(data["created_at"]) \
308
+ if data.get("created_at") is not None else None
309
+ updated_at = parse(data["updated_at"]) \
310
+ if data.get("updated_at") is not None else None
311
+ triggered_at = parse(data["triggered_at"]) \
312
+ if data.get("triggered_at") is not None else None
313
+ high_water_mark_date = parse(data.get("high_water_mark_date")) \
314
+ if data.get("high_water_mark_date") is not None else None
315
+
316
+ # Make sure all the dates are timezone utc aware
317
+ if created_at and created_at.tzinfo is None:
318
+ created_at = created_at.replace(tzinfo=timezone.utc)
319
+ if updated_at and updated_at.tzinfo is None:
320
+ updated_at = updated_at.replace(tzinfo=timezone.utc)
321
+ if triggered_at and triggered_at.tzinfo is None:
322
+ triggered_at = triggered_at.replace(tzinfo=timezone.utc)
323
+ if high_water_mark_date and high_water_mark_date.tzinfo is None:
324
+ high_water_mark_date = high_water_mark_date.replace(
325
+ tzinfo=timezone.utc
326
+ )
327
+
274
328
  return TradeTakeProfit(
275
329
  trade_id=data.get("trade_id"),
276
- trade_risk_type=TradeRiskType.from_string(
277
- data.get("trade_risk_type")
278
- ),
330
+ trailing=data.get("trailing"),
279
331
  percentage=data.get("percentage"),
280
332
  open_price=data.get("open_price"),
281
333
  total_amount_trade=data.get("total_amount_trade"),
282
334
  sell_percentage=data.get("sell_percentage", 100),
283
335
  active=data.get("active", True),
336
+ triggered=data.get("triggered", False),
337
+ triggered_at=triggered_at,
284
338
  sell_prices=data.get("sell_prices"),
285
339
  sell_dates=data.get("sell_dates"),
286
340
  sell_amount=data.get("sell_amount"),
287
- high_water_mark_date=data.get("high_water_mark_date")
341
+ high_water_mark=data.get("high_water_mark"),
342
+ high_water_mark_date=high_water_mark_date,
343
+ created_at=created_at,
344
+ updated_at=updated_at
288
345
  )
289
346
 
290
347
  def __repr__(self):
291
348
  return self.repr(
292
349
  trade_id=self.trade_id,
293
- trade_risk_type=self.trade_risk_type,
350
+ trailing=self.trailing,
294
351
  percentage=self.percentage,
295
352
  open_price=self.open_price,
296
353
  sell_percentage=self.sell_percentage,
297
354
  high_water_mark=self.high_water_mark,
355
+ high_water_mark_date=self.high_water_mark_date,
356
+ triggered=self.triggered,
357
+ triggered_at=self.triggered_at,
298
358
  take_profit_price=self.take_profit_price,
299
359
  sell_amount=self.sell_amount,
300
360
  sold_amount=self.sold_amount,
301
361
  active=self.active,
302
- sell_prices=self.sell_prices
362
+ sell_prices=self.sell_prices,
363
+ created_at=self.created_at,
364
+ updated_at=self.updated_at
303
365
  )