investing-algorithm-framework 1.3.1__py3-none-any.whl → 7.25.6__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 (282) hide show
  1. investing_algorithm_framework/__init__.py +195 -16
  2. investing_algorithm_framework/analysis/__init__.py +16 -0
  3. investing_algorithm_framework/analysis/backtest_data_ranges.py +202 -0
  4. investing_algorithm_framework/analysis/data.py +170 -0
  5. investing_algorithm_framework/analysis/markdown.py +91 -0
  6. investing_algorithm_framework/analysis/ranking.py +298 -0
  7. investing_algorithm_framework/app/__init__.py +31 -4
  8. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  9. investing_algorithm_framework/app/algorithm/algorithm.py +193 -0
  10. investing_algorithm_framework/app/algorithm/algorithm_factory.py +118 -0
  11. investing_algorithm_framework/app/app.py +2233 -264
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1724 -0
  14. investing_algorithm_framework/app/eventloop.py +620 -0
  15. investing_algorithm_framework/app/reporting/__init__.py +27 -0
  16. investing_algorithm_framework/app/reporting/ascii.py +921 -0
  17. investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
  18. investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
  19. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  20. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  21. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
  22. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  23. investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
  24. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  25. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
  26. investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
  27. investing_algorithm_framework/app/reporting/generate.py +185 -0
  28. investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
  29. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
  30. investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
  31. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
  32. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  33. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  34. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  35. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  36. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +6 -3
  37. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
  38. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +2 -1
  39. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
  40. investing_algorithm_framework/app/stateless/exception_handler.py +1 -1
  41. investing_algorithm_framework/app/strategy.py +873 -52
  42. investing_algorithm_framework/app/task.py +5 -3
  43. investing_algorithm_framework/app/web/__init__.py +2 -1
  44. investing_algorithm_framework/app/web/controllers/__init__.py +2 -2
  45. investing_algorithm_framework/app/web/controllers/orders.py +4 -3
  46. investing_algorithm_framework/app/web/controllers/portfolio.py +1 -1
  47. investing_algorithm_framework/app/web/controllers/positions.py +3 -3
  48. investing_algorithm_framework/app/web/create_app.py +4 -2
  49. investing_algorithm_framework/app/web/error_handler.py +1 -1
  50. investing_algorithm_framework/app/web/schemas/order.py +2 -2
  51. investing_algorithm_framework/app/web/schemas/position.py +1 -0
  52. investing_algorithm_framework/cli/__init__.py +0 -0
  53. investing_algorithm_framework/cli/cli.py +231 -0
  54. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
  55. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  56. investing_algorithm_framework/cli/initialize_app.py +603 -0
  57. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  58. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  59. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  60. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  61. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  62. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  63. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  64. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  65. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  66. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  67. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  68. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  69. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  70. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  71. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  72. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  73. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  74. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  75. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  76. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  77. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  78. investing_algorithm_framework/cli/validate_backtest_checkpoints.py +197 -0
  79. investing_algorithm_framework/create_app.py +43 -9
  80. investing_algorithm_framework/dependency_container.py +121 -33
  81. investing_algorithm_framework/domain/__init__.py +109 -22
  82. investing_algorithm_framework/domain/algorithm_id.py +69 -0
  83. investing_algorithm_framework/domain/backtesting/__init__.py +25 -0
  84. investing_algorithm_framework/domain/backtesting/backtest.py +548 -0
  85. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +113 -0
  86. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +241 -0
  87. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +470 -0
  88. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  89. investing_algorithm_framework/domain/backtesting/backtest_run.py +663 -0
  90. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  91. investing_algorithm_framework/domain/backtesting/backtest_utils.py +198 -0
  92. investing_algorithm_framework/domain/backtesting/combine_backtests.py +392 -0
  93. investing_algorithm_framework/domain/config.py +60 -138
  94. investing_algorithm_framework/domain/constants.py +23 -34
  95. investing_algorithm_framework/domain/data_provider.py +334 -0
  96. investing_algorithm_framework/domain/data_structures.py +42 -0
  97. investing_algorithm_framework/domain/decimal_parsing.py +40 -0
  98. investing_algorithm_framework/domain/exceptions.py +51 -1
  99. investing_algorithm_framework/domain/models/__init__.py +29 -14
  100. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  101. investing_algorithm_framework/domain/models/base_model.py +3 -1
  102. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  103. investing_algorithm_framework/domain/models/data/data_source.py +222 -0
  104. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  105. investing_algorithm_framework/domain/models/event.py +35 -0
  106. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  107. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  108. investing_algorithm_framework/domain/models/order/__init__.py +3 -4
  109. investing_algorithm_framework/domain/models/order/order.py +243 -86
  110. investing_algorithm_framework/domain/models/order/order_status.py +2 -2
  111. investing_algorithm_framework/domain/models/order/order_type.py +1 -3
  112. investing_algorithm_framework/domain/models/portfolio/__init__.py +7 -2
  113. investing_algorithm_framework/domain/models/portfolio/portfolio.py +134 -1
  114. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +37 -37
  115. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
  116. investing_algorithm_framework/domain/models/position/__init__.py +3 -2
  117. investing_algorithm_framework/domain/models/position/position.py +29 -0
  118. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  119. investing_algorithm_framework/domain/models/position/{position_cost.py → position_snapshot.py} +16 -8
  120. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  121. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  122. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  123. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  124. investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
  125. investing_algorithm_framework/domain/models/time_frame.py +94 -98
  126. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  127. investing_algorithm_framework/domain/models/time_unit.py +111 -2
  128. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  129. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  130. investing_algorithm_framework/domain/models/trade/__init__.py +11 -0
  131. investing_algorithm_framework/domain/models/trade/trade.py +389 -0
  132. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  133. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
  134. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
  135. investing_algorithm_framework/domain/order_executor.py +112 -0
  136. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  137. investing_algorithm_framework/domain/services/__init__.py +11 -0
  138. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  139. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  140. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  141. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  142. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  143. investing_algorithm_framework/domain/strategy.py +1 -29
  144. investing_algorithm_framework/domain/utils/__init__.py +16 -4
  145. investing_algorithm_framework/domain/utils/csv.py +22 -0
  146. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  147. investing_algorithm_framework/domain/utils/dates.py +57 -0
  148. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  149. investing_algorithm_framework/domain/utils/polars.py +53 -0
  150. investing_algorithm_framework/domain/utils/random.py +29 -0
  151. investing_algorithm_framework/download_data.py +244 -0
  152. investing_algorithm_framework/infrastructure/__init__.py +39 -11
  153. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  154. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1152 -0
  155. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  156. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  157. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  158. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +87 -13
  159. investing_algorithm_framework/infrastructure/models/__init__.py +13 -4
  160. investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
  161. investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -2
  162. investing_algorithm_framework/infrastructure/models/order/order.py +73 -73
  163. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  164. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  165. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +3 -2
  166. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
  167. investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +57 -3
  168. investing_algorithm_framework/infrastructure/models/position/__init__.py +2 -2
  169. investing_algorithm_framework/infrastructure/models/position/position.py +16 -11
  170. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
  171. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  172. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  173. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
  174. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
  175. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  176. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  177. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  178. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  179. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  180. investing_algorithm_framework/infrastructure/repositories/__init__.py +13 -5
  181. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  182. investing_algorithm_framework/infrastructure/repositories/order_repository.py +32 -19
  183. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +2 -2
  184. investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
  185. investing_algorithm_framework/infrastructure/repositories/position_repository.py +47 -4
  186. investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
  187. investing_algorithm_framework/infrastructure/repositories/repository.py +85 -31
  188. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  189. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
  190. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
  191. investing_algorithm_framework/infrastructure/services/__init__.py +9 -2
  192. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  193. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +193 -0
  194. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  195. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  196. investing_algorithm_framework/infrastructure/services/backtesting/__init__.py +9 -0
  197. investing_algorithm_framework/infrastructure/services/backtesting/backtest_service.py +2596 -0
  198. investing_algorithm_framework/infrastructure/services/backtesting/event_backtest_service.py +285 -0
  199. investing_algorithm_framework/infrastructure/services/backtesting/vector_backtest_service.py +468 -0
  200. investing_algorithm_framework/services/__init__.py +127 -10
  201. investing_algorithm_framework/services/configuration_service.py +95 -0
  202. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  203. investing_algorithm_framework/services/data_providers/data_provider_service.py +1058 -0
  204. investing_algorithm_framework/services/market_credential_service.py +40 -0
  205. investing_algorithm_framework/services/metrics/__init__.py +119 -0
  206. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  207. investing_algorithm_framework/services/metrics/beta.py +0 -0
  208. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  209. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  210. investing_algorithm_framework/services/metrics/drawdown.py +218 -0
  211. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  212. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  213. investing_algorithm_framework/services/metrics/generate.py +358 -0
  214. investing_algorithm_framework/services/metrics/mean_daily_return.py +84 -0
  215. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  216. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  217. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  218. investing_algorithm_framework/services/metrics/returns.py +452 -0
  219. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  220. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  221. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  222. investing_algorithm_framework/services/metrics/standard_deviation.py +156 -0
  223. investing_algorithm_framework/services/metrics/trades.py +473 -0
  224. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  225. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  226. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  227. investing_algorithm_framework/services/metrics/volatility.py +118 -0
  228. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  229. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  230. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  231. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  232. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  233. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  234. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  235. investing_algorithm_framework/services/{portfolio_configuration_service.py → portfolios/portfolio_configuration_service.py} +27 -12
  236. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  237. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  238. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  239. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  240. investing_algorithm_framework/services/positions/__init__.py +7 -0
  241. investing_algorithm_framework/services/positions/position_service.py +210 -0
  242. investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
  243. investing_algorithm_framework/services/repository_service.py +8 -2
  244. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  245. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +117 -0
  246. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  247. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  248. investing_algorithm_framework/services/trade_service/__init__.py +9 -0
  249. investing_algorithm_framework/services/trade_service/trade_service.py +1099 -0
  250. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  251. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  252. investing_algorithm_framework-7.25.6.dist-info/METADATA +535 -0
  253. investing_algorithm_framework-7.25.6.dist-info/RECORD +268 -0
  254. {investing_algorithm_framework-1.3.1.dist-info → investing_algorithm_framework-7.25.6.dist-info}/WHEEL +1 -2
  255. investing_algorithm_framework-7.25.6.dist-info/entry_points.txt +3 -0
  256. investing_algorithm_framework/app/algorithm.py +0 -410
  257. investing_algorithm_framework/domain/models/market_data/__init__.py +0 -11
  258. investing_algorithm_framework/domain/models/market_data/asset_price.py +0 -50
  259. investing_algorithm_framework/domain/models/market_data/ohlcv.py +0 -76
  260. investing_algorithm_framework/domain/models/market_data/order_book.py +0 -63
  261. investing_algorithm_framework/domain/models/market_data/ticker.py +0 -92
  262. investing_algorithm_framework/domain/models/order/order_fee.py +0 -45
  263. investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
  264. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -205
  265. investing_algorithm_framework/domain/singleton.py +0 -9
  266. investing_algorithm_framework/infrastructure/models/order/order_fee.py +0 -21
  267. investing_algorithm_framework/infrastructure/models/position/position_cost.py +0 -32
  268. investing_algorithm_framework/infrastructure/repositories/order_fee_repository.py +0 -15
  269. investing_algorithm_framework/infrastructure/repositories/position_cost_repository.py +0 -16
  270. investing_algorithm_framework/infrastructure/services/market_service.py +0 -422
  271. investing_algorithm_framework/services/market_data_service.py +0 -75
  272. investing_algorithm_framework/services/order_service.py +0 -464
  273. investing_algorithm_framework/services/portfolio_service.py +0 -105
  274. investing_algorithm_framework/services/position_cost_service.py +0 -5
  275. investing_algorithm_framework/services/position_service.py +0 -50
  276. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -219
  277. investing_algorithm_framework/setup_logging.py +0 -40
  278. investing_algorithm_framework-1.3.1.dist-info/AUTHORS.md +0 -8
  279. investing_algorithm_framework-1.3.1.dist-info/METADATA +0 -172
  280. investing_algorithm_framework-1.3.1.dist-info/RECORD +0 -103
  281. investing_algorithm_framework-1.3.1.dist-info/top_level.txt +0 -1
  282. {investing_algorithm_framework-1.3.1.dist-info → investing_algorithm_framework-7.25.6.dist-info}/LICENSE +0 -0
@@ -0,0 +1,1099 @@
1
+ import logging
2
+ from datetime import datetime, timezone
3
+ from queue import PriorityQueue
4
+ from typing import Union
5
+
6
+ from investing_algorithm_framework.domain import OrderStatus, TradeStatus, \
7
+ Trade, OperationalException, OrderType, TradeTakeProfit, \
8
+ TradeStopLoss, OrderSide, Environment, ENVIRONMENT, PeekableQueue, \
9
+ DataType, INDEX_DATETIME, random_number, random_string
10
+ from investing_algorithm_framework.services.repository_service import \
11
+ RepositoryService
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class TradeService(RepositoryService):
17
+ """
18
+ Trade service class to handle trade related operations. This class
19
+ is responsible for creating, updating, and deleting trades. It also
20
+ takes care of keeping track of all sell transactions that are
21
+ associated with a trade.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ trade_repository,
27
+ order_repository,
28
+ trade_stop_loss_repository,
29
+ trade_take_profit_repository,
30
+ position_repository,
31
+ portfolio_repository,
32
+ configuration_service,
33
+ order_metadata_repository
34
+ ):
35
+ super(TradeService, self).__init__(trade_repository)
36
+ self.order_repository = order_repository
37
+ self.portfolio_repository = portfolio_repository
38
+ self.position_repository = position_repository
39
+ self.configuration_service = configuration_service
40
+ self.trade_stop_loss_repository = trade_stop_loss_repository
41
+ self.trade_take_profit_repository = trade_take_profit_repository
42
+ self.order_metadata_repository = order_metadata_repository
43
+
44
+ def create_trade_from_buy_order(self, buy_order) -> Union[Trade, None]:
45
+ """
46
+ Function to create a trade from a buy order. If the given buy
47
+ order has its status set to CANCELED, EXPIRED, or REJECTED,
48
+ the trade object will not be created. If the given buy
49
+ order has its status set to CLOSED or OPEN, the trade object
50
+ will be created. The amount will be set to the filled amount.
51
+
52
+ Args:
53
+ buy_order: Order object representing the buy order
54
+
55
+ Returns:
56
+ Union[Trade, None] Representing the created trade object or None
57
+ """
58
+
59
+ if buy_order.status in \
60
+ [
61
+ OrderStatus.CANCELED.value,
62
+ OrderStatus.EXPIRED.value,
63
+ OrderStatus.REJECTED.value
64
+ ]:
65
+ return None
66
+
67
+ data = {
68
+ "buy_order": buy_order,
69
+ "target_symbol": buy_order.target_symbol,
70
+ "trading_symbol": buy_order.trading_symbol,
71
+ "amount": buy_order.get_amount(),
72
+ "available_amount": buy_order.get_filled(),
73
+ "filled_amount": buy_order.get_filled(),
74
+ "remaining": buy_order.get_remaining(),
75
+ "opened_at": buy_order.created_at,
76
+ "cost": buy_order.get_filled() * buy_order.price
77
+ }
78
+
79
+ if buy_order.get_filled() > 0:
80
+ data["status"] = TradeStatus.OPEN.value
81
+ data["cost"] = buy_order.filled * buy_order.price
82
+
83
+ return self.create(data)
84
+
85
+ def _create_trade_metadata_with_sell_order(self, sell_order):
86
+ """
87
+ Function to create trade metadata with only a sell order.
88
+ This function will create all metadata objects for the trades
89
+ that are closed with the sell order amount.
90
+
91
+ Args:
92
+ sell_order: Order object representing the sell order
93
+
94
+ Returns:
95
+ None
96
+ """
97
+ position = self.position_repository.find({
98
+ "order_id": sell_order.id
99
+ })
100
+ portfolio_id = position.portfolio_id
101
+ matching_trades = self.get_all({
102
+ "status": TradeStatus.OPEN.value,
103
+ "target_symbol": sell_order.target_symbol,
104
+ "portfolio_id": portfolio_id
105
+ })
106
+ updated_at = sell_order.updated_at
107
+ total_available_to_close = 0
108
+ amount_to_close = sell_order.amount
109
+ trade_queue = PriorityQueue()
110
+ sell_order_id = sell_order.id
111
+ sell_price = sell_order.price
112
+
113
+ for trade in matching_trades:
114
+ if trade.available_amount > 0:
115
+ total_available_to_close += trade.available_amount
116
+ trade_queue.put(trade)
117
+
118
+ if total_available_to_close < amount_to_close:
119
+ raise OperationalException(
120
+ "Not enough amount to close in trades."
121
+ )
122
+
123
+ # Create order metadata object
124
+ while amount_to_close > 0 and not trade_queue.empty():
125
+ trade = trade_queue.get()
126
+ trade_id = trade.id
127
+ available_to_close = trade.available_amount
128
+
129
+ if amount_to_close >= available_to_close:
130
+ amount_to_close = amount_to_close - available_to_close
131
+ cost = trade.open_price * available_to_close
132
+ net_gain = (sell_price * available_to_close) - cost
133
+ update_data = {
134
+ "available_amount": 0,
135
+ "orders": trade.orders.append(sell_order),
136
+ "updated_at": updated_at,
137
+ "closed_at": updated_at,
138
+ "net_gain": trade.net_gain + net_gain
139
+ }
140
+
141
+ if trade.remaining == 0:
142
+ update_data["status"] = TradeStatus.CLOSED.value
143
+
144
+ self.update(trade_id, update_data)
145
+ self.repository.add_order_to_trade(trade, sell_order)
146
+
147
+ # Create metadata object
148
+ self.order_metadata_repository.\
149
+ create({
150
+ "order_id": sell_order_id,
151
+ "trade_id": trade_id,
152
+ "amount": available_to_close,
153
+ "amount_pending": available_to_close,
154
+ })
155
+ else:
156
+ to_be_closed = amount_to_close
157
+ cost = trade.open_price * to_be_closed
158
+ net_gain = (sell_price * to_be_closed) - cost
159
+
160
+ self.update(
161
+ trade_id, {
162
+ "available_amount":
163
+ trade.available_amount - to_be_closed,
164
+ "orders": trade.orders.append(sell_order),
165
+ "updated_at": updated_at,
166
+ "net_gain": trade.net_gain + net_gain
167
+ }
168
+ )
169
+ self.repository.add_order_to_trade(trade, sell_order)
170
+
171
+ # Create an order metadata object
172
+ self.order_metadata_repository.\
173
+ create({
174
+ "order_id": sell_order_id,
175
+ "trade_id": trade_id,
176
+ "amount": to_be_closed,
177
+ "amount_pending": to_be_closed,
178
+ })
179
+
180
+ amount_to_close = 0
181
+
182
+ def _create_stop_loss_metadata_with_sell_order(
183
+ self, sell_order_id, stop_losses
184
+ ):
185
+ """
186
+ """
187
+ sell_order = self.order_repository.get(sell_order_id)
188
+
189
+ for stop_loss_data in stop_losses:
190
+
191
+ self.order_metadata_repository.\
192
+ create({
193
+ "order_id": sell_order.id,
194
+ "stop_loss_id": stop_loss_data["stop_loss_id"],
195
+ "amount": stop_loss_data["amount"],
196
+ "amount_pending": stop_loss_data["amount"]
197
+ })
198
+
199
+ def _create_take_profit_metadata_with_sell_order(
200
+ self, sell_order_id, take_profits
201
+ ):
202
+ """
203
+ """
204
+ sell_order = self.order_repository.get(sell_order_id)
205
+
206
+ for take_profit_data in take_profits:
207
+
208
+ self.order_metadata_repository.\
209
+ create({
210
+ "order_id": sell_order.id,
211
+ "take_profit_id": take_profit_data["take_profit_id"],
212
+ "amount": take_profit_data["amount"],
213
+ "amount_pending": take_profit_data["amount"]
214
+ })
215
+
216
+ def update(self, trade_id, data) -> Trade:
217
+ """
218
+ Function to update a trade object. This function will update
219
+ the trade object with the given data.
220
+
221
+ Args:
222
+ trade_id: int representing the id of the trade object
223
+ data: dict representing the data that should be updated
224
+
225
+ Returns:
226
+ Trade object
227
+ """
228
+
229
+ # Update the stop losses and take profits if last reported price
230
+ # is updated
231
+ if "last_reported_price" in data:
232
+ trade = self.get(trade_id)
233
+ stop_losses = trade.stop_losses
234
+ to_be_saved_stop_losses = []
235
+ take_profits = trade.take_profits
236
+ to_be_saved_take_profits = []
237
+
238
+ # Check if 'update_at' attribute is in data
239
+
240
+ if 'last_reported_price_date' in data:
241
+ last_reported_price_date = data["last_reported_price_date"]
242
+ else:
243
+
244
+ # Check if config environment has value BACKTEST
245
+ config = self.configuration_service.get_config()
246
+ environment = config[ENVIRONMENT]
247
+
248
+ if Environment.BACKTEST.equals(environment):
249
+ last_reported_price_date = \
250
+ config[INDEX_DATETIME]
251
+ else:
252
+ last_reported_price_date = \
253
+ datetime.now(tz=timezone.utc)
254
+
255
+ for stop_loss in stop_losses:
256
+
257
+ if stop_loss.active:
258
+ stop_loss.update_with_last_reported_price(
259
+ data["last_reported_price"], last_reported_price_date
260
+ )
261
+ to_be_saved_stop_losses.append(stop_loss)
262
+
263
+ for take_profit in take_profits:
264
+
265
+ if take_profit.active:
266
+ take_profit.update_with_last_reported_price(
267
+ data["last_reported_price"], last_reported_price_date
268
+ )
269
+ to_be_saved_take_profits.append(take_profit)
270
+
271
+ self.trade_stop_loss_repository\
272
+ .save_objects(to_be_saved_stop_losses)
273
+
274
+ self.trade_take_profit_repository\
275
+ .save_objects(to_be_saved_take_profits)
276
+
277
+ return super(TradeService, self).update(trade_id, data)
278
+
279
+ def _create_trade_metadata_with_sell_order_and_trades(
280
+ self, sell_order, trades
281
+ ):
282
+ """
283
+ Function to create trade metadata with a sell order and trades.
284
+
285
+ The metadata objects function as a link between the trades and
286
+ the sell order. The metadata objects are used to keep track
287
+ of the trades that are closed with the sell order.
288
+
289
+ A single sell order can close or partially close multiple trades.
290
+ Therefore it is important to keep track of the trades that are
291
+ closed with the sell order. The metadata objects are used to
292
+ keep track of this relationship.
293
+
294
+
295
+ """
296
+ sell_order_id = sell_order.id
297
+ updated_at = sell_order.updated_at
298
+ sell_amount = sell_order.amount
299
+ sell_price = sell_order.price
300
+
301
+ for trade_data in trades:
302
+ trade = self.get(trade_data["trade_id"])
303
+ trade_id = trade.id
304
+ open_price = trade.open_price
305
+ old_net_gain = trade.net_gain
306
+ available_amount = trade.available_amount
307
+ filled_amount = trade.filled_amount
308
+ amount = trade.amount
309
+
310
+ self.order_metadata_repository.\
311
+ create({
312
+ "order_id": sell_order_id,
313
+ "trade_id": trade_data["trade_id"],
314
+ "amount": trade_data["amount"],
315
+ "amount_pending": trade_data["amount"]
316
+ })
317
+
318
+ # Add the sell order to the trade
319
+ self.repository.add_order_to_trade(trade, sell_order)
320
+
321
+ # Update the trade
322
+ net_gain = (sell_price * sell_amount) - open_price * sell_amount
323
+ available_amount = available_amount - trade_data["amount"]
324
+ trade_updated_data = {
325
+ "available_amount": available_amount,
326
+ "updated_at": updated_at,
327
+ "net_gain": old_net_gain + net_gain
328
+ }
329
+
330
+ if available_amount == 0 and filled_amount == amount:
331
+ trade_updated_data["status"] = TradeStatus.CLOSED.value
332
+ trade_updated_data["closed_at"] = updated_at
333
+ else:
334
+ trade_updated_data["status"] = TradeStatus.OPEN.value
335
+
336
+ # Update the trade object
337
+ self.update(trade_id, trade_updated_data)
338
+
339
+ def create_order_metadata_with_trade_context(
340
+ self, sell_order, trades=None, stop_losses=None, take_profits=None
341
+ ):
342
+ """
343
+ Function to create order metadata for trade related models.
344
+
345
+ If only the sell order is provided, we assume that the sell order
346
+ is initiated by a client of the order service. In this case we
347
+ create only metadata objects for the trades based on size of the
348
+ sell order.
349
+
350
+ If also stop losses and take profits are provided, we assume that
351
+ the sell order is initiated a stop loss or take profit. In this case
352
+ we create metadata objects for the trades, stop losses,
353
+ and take profits.
354
+
355
+ If the trades param is provided, we assume that the sell order is
356
+ based on either a stop loss or take profit or a closing of a trade.
357
+ In this case we create also the metadata objects for the trades,
358
+
359
+ As part of this function, we will also update the position cost.
360
+
361
+ Scenario 1: Sell order without trades, stop losses, and take profits
362
+ - Use the sell amount to create all trade metadata objects
363
+ - Update the position cost
364
+
365
+ Scenario 2: Sell order with trades
366
+ - We assume that the sell amount is same as the total amount
367
+ of the trades
368
+ - Use the trades to create all trade metadata objects
369
+ - Update trade object remaining amount
370
+ - Update the position cost
371
+
372
+ Scenario 3: Sell order with trades, stop losses, and take profits
373
+ - We assume that the sell amount is same as the total
374
+ amount of the trades
375
+ - Use the trades to create all metadata objects
376
+ - Update trade object remaining amount
377
+ - Use the stop losses to create all metadata objects
378
+ - Use the take profits to create all metadata objects
379
+ - Update the position cost
380
+
381
+ Args:
382
+ sell_order: Order object representing the sell order that has
383
+ been created
384
+ trades: List of Trade objects representing the trades that
385
+ are associated with the sell order. Default is None.
386
+ stop_losses: List of StopLoss objects representing the stop
387
+ losses that are associated with the sell order. Default
388
+ is None.
389
+ take_profits: List of TakeProfit objects representing the take
390
+ profits that are associated with the sell order. Default
391
+ is None.
392
+
393
+ Returns:
394
+ None
395
+ """
396
+ sell_order_id = sell_order.id
397
+ sell_price = sell_order.price
398
+ sell_amount = sell_order.amount
399
+
400
+ if (trades is None or len(trades) == 0) \
401
+ and (stop_losses is None or len(stop_losses) == 0) \
402
+ and (take_profits is None or len(take_profits) == 0):
403
+ self._create_trade_metadata_with_sell_order(sell_order)
404
+ else:
405
+
406
+ if stop_losses is not None:
407
+ self._create_stop_loss_metadata_with_sell_order(
408
+ sell_order_id, stop_losses
409
+ )
410
+
411
+ if take_profits is not None:
412
+ self._create_take_profit_metadata_with_sell_order(
413
+ sell_order_id, take_profits
414
+ )
415
+
416
+ if trades is not None:
417
+ self._create_trade_metadata_with_sell_order_and_trades(
418
+ sell_order, trades
419
+ )
420
+
421
+ # Retrieve all trades metadata objects
422
+ order_metadatas = self.order_metadata_repository.get_all({
423
+ "order_id": sell_order_id
424
+ })
425
+
426
+ # Update the position cost
427
+ position = self.position_repository.find({
428
+ "order_id": sell_order_id
429
+ })
430
+
431
+ # Update position
432
+ cost = 0
433
+ net_gain = 0
434
+ for metadata in order_metadatas:
435
+ if metadata.trade_id is not None:
436
+ trade = self.get(metadata.trade_id)
437
+ cost += trade.open_price * metadata.amount
438
+ net_gain += (sell_price * metadata.amount) - cost
439
+
440
+ position.cost -= cost
441
+ self.position_repository.save(position)
442
+
443
+ # Update the net gain, net size of the portfolio
444
+ portfolio = self.portfolio_repository.get(position.portfolio_id)
445
+ portfolio.total_net_gain += net_gain
446
+ portfolio.net_size += net_gain
447
+ portfolio.total_revenue += sell_price * sell_amount
448
+ self.portfolio_repository.save(portfolio)
449
+
450
+ def update_trade_with_removed_sell_order(
451
+ self, sell_order
452
+ ) -> Trade:
453
+ """
454
+ This function updates a trade with a removed sell order that belongs
455
+ to the trade. This function uses the order metadata objects to
456
+ update the trade object. The function will update the trade object
457
+ available amount, cost, and net gain. The function will also
458
+ update the stop loss and take profit objects that are associated
459
+ with the trade object. The function will update the position cost
460
+ and the portfolio net gain.
461
+
462
+ Args:
463
+ sell_order (Order): Order object representing the sell order
464
+ that has been removed
465
+
466
+ Returns:
467
+ Trade: Trade object representing the updated trade object
468
+ """
469
+ position_cost = 0
470
+ total_net_gain = 0
471
+
472
+ # Get all order metadata objects that are associated with
473
+ # the sell order
474
+ order_metadatas = self.order_metadata_repository.get_all({
475
+ "order_id": sell_order.id
476
+ })
477
+
478
+ for metadata in order_metadatas:
479
+ # If trade id is not None, update the trade object
480
+ if metadata.trade_id is not None:
481
+ trade = self.get(metadata.trade_id)
482
+ cost = metadata.amount_pending * trade.open_price
483
+ net_gain = (sell_order.price * metadata.amount_pending) - cost
484
+ trade.available_amount += metadata.amount_pending
485
+ trade.status = TradeStatus.OPEN.value
486
+ trade.updated_at = sell_order.updated_at
487
+ trade.net_gain -= net_gain
488
+ trade.cost += cost
489
+ trade = self.save(trade)
490
+
491
+ # Update the position cost
492
+ position_cost += cost
493
+ total_net_gain += net_gain
494
+
495
+ if metadata.stop_loss_id is not None:
496
+ stop_loss = self.trade_stop_loss_repository\
497
+ .get(metadata.stop_loss_id)
498
+ stop_loss.sold_amount -= metadata.amount_pending
499
+ stop_loss.remove_sell_price(
500
+ sell_order.price, sell_order.created_at
501
+ )
502
+
503
+ if stop_loss.sold_amount < stop_loss.sell_amount:
504
+ stop_loss.active = True
505
+ stop_loss.high_water_mark = None
506
+
507
+ self.trade_stop_loss_repository.save(stop_loss)
508
+
509
+ if metadata.take_profit_id is not None:
510
+ take_profit = self.trade_take_profit_repository\
511
+ .get(metadata.take_profit_id)
512
+ take_profit.sold_amount -= metadata.amount_pending
513
+ take_profit.remove_sell_price(
514
+ sell_order.price, sell_order.created_at
515
+ )
516
+
517
+ if take_profit.sold_amount < take_profit.sell_amount:
518
+ take_profit.active = True
519
+ take_profit.high_water_mark = None
520
+
521
+ self.trade_take_profit_repository.save(take_profit)
522
+
523
+ # Update the position cost
524
+ position = self.position_repository.find({
525
+ "order_id": sell_order.id
526
+ })
527
+ position.cost += position_cost
528
+ self.position_repository.save(position)
529
+
530
+ # Update the net gain of the portfolio
531
+ portfolio = self.portfolio_repository.get(position.portfolio_id)
532
+ portfolio.total_net_gain -= total_net_gain
533
+ portfolio.net_size -= total_net_gain
534
+ self.portfolio_repository.save(portfolio)
535
+ return trade
536
+
537
+ def update_trade_with_buy_order(
538
+ self, filled_difference, buy_order
539
+ ) -> Trade:
540
+ """
541
+ Function to update a trade from a buy order. This function
542
+ checks if a trade exists for the buy order. If the given buy
543
+ order has its status set to CANCLED, EXPIRED, or REJECTED, the
544
+ trade will object will be removed. If the given buy order has
545
+ its status set to CLOSED or OPEN, the amount and
546
+ remaining of the trade object will be updated.
547
+
548
+ Args:
549
+ filled_difference: float representing the difference between the
550
+ filled amount of the buy order and the filled amount
551
+ of the trade
552
+ buy_order: Order object representing the buy order
553
+
554
+ Returns:
555
+ Trade object
556
+ """
557
+ trade = self.find({"buy_order": buy_order.id})
558
+ filled = buy_order.get_filled()
559
+ amount = buy_order.get_amount()
560
+
561
+ if filled is None:
562
+ filled = trade.filled_amount + filled_difference
563
+
564
+ remaining = buy_order.get_remaining()
565
+
566
+ if remaining is None:
567
+ remaining = trade.remaining - filled_difference
568
+
569
+ if trade is None:
570
+ raise OperationalException(
571
+ "Trade does not exist for buy order."
572
+ )
573
+
574
+ status = buy_order.get_status()
575
+
576
+ if status in \
577
+ [
578
+ OrderStatus.CANCELED.value,
579
+ OrderStatus.EXPIRED.value,
580
+ OrderStatus.REJECTED.value
581
+ ]:
582
+ return self.delete(trade.id)
583
+
584
+ trade = self.find({"order_id": buy_order.id})
585
+ updated_data = {
586
+ "available_amount": trade.available_amount + filled_difference,
587
+ "filled_amount": filled,
588
+ "remaining": remaining,
589
+ "cost": trade.cost + filled_difference * buy_order.price
590
+ }
591
+
592
+ if amount != trade.amount:
593
+ updated_data["amount"] = amount
594
+ updated_data["cost"] = amount * buy_order.price
595
+
596
+ if filled_difference > 0:
597
+ updated_data["status"] = TradeStatus.OPEN.value
598
+
599
+ trade = self.update(trade.id, updated_data)
600
+ return trade
601
+
602
+ def update_trade_with_filled_sell_order(
603
+ self, filled_difference, sell_order
604
+ ) -> Trade:
605
+ """
606
+ Function to update a trade with a filled sell order. This
607
+ function will update all the metadata objects that where
608
+ created by the sell order.
609
+
610
+ Args:
611
+ filled_difference: float representing the difference between
612
+ the filled amount of the sell order and the filled amount
613
+ of the trade
614
+ sell_order: Order object representing the sell order
615
+
616
+ Returns:
617
+ Trade object
618
+ """
619
+ # Update all metadata objects
620
+ metadata_objects = self.order_metadata_repository.get_all({
621
+ "order_id": sell_order.id
622
+ })
623
+
624
+ trade_filled_difference = filled_difference
625
+ stop_loss_filled_difference = filled_difference
626
+ take_profit_filled_difference = filled_difference
627
+ total_amount_in_metadata = 0
628
+ trade_metadata_objects = []
629
+
630
+ for metadata_object in metadata_objects:
631
+ # Update the trade metadata object
632
+ if metadata_object.trade_id is not None \
633
+ and trade_filled_difference > 0:
634
+
635
+ trade_metadata_objects.append(metadata_object)
636
+ total_amount_in_metadata += metadata_object.amount
637
+
638
+ if metadata_object.amount_pending >= trade_filled_difference:
639
+ amount = trade_filled_difference
640
+ trade_filled_difference = 0
641
+ else:
642
+ amount = metadata_object.amount_pending
643
+ trade_filled_difference -= amount
644
+
645
+ metadata_object.amount_pending -= amount
646
+ self.order_metadata_repository.save(metadata_object)
647
+
648
+ if metadata_object.stop_loss_id is not None \
649
+ and stop_loss_filled_difference > 0:
650
+
651
+ if (
652
+ metadata_object.amount_pending >=
653
+ stop_loss_filled_difference
654
+ ):
655
+ amount = stop_loss_filled_difference
656
+ stop_loss_filled_difference = 0
657
+ else:
658
+ amount = metadata_object.amount_pending
659
+ stop_loss_filled_difference -= amount
660
+
661
+ metadata_object.amount_pending -= amount
662
+ self.order_metadata_repository.save(metadata_object)
663
+
664
+ if metadata_object.take_profit_id is not None \
665
+ and take_profit_filled_difference > 0:
666
+
667
+ if (
668
+ metadata_object.amount_pending >=
669
+ take_profit_filled_difference
670
+ ):
671
+ amount = take_profit_filled_difference
672
+ take_profit_filled_difference = 0
673
+ else:
674
+ amount = metadata_object.amount_pending
675
+ take_profit_filled_difference -= amount
676
+
677
+ metadata_object.amount_pending -= amount
678
+ self.order_metadata_repository.save(metadata_object)
679
+
680
+ # Update trade available amount if the total amount in metadata
681
+ # is not equal to the sell order amount
682
+ if total_amount_in_metadata != sell_order.amount:
683
+ difference = sell_order.amount - total_amount_in_metadata
684
+ trades = []
685
+
686
+ for metadata_object in trade_metadata_objects:
687
+ trade = self.get(metadata_object.trade_id)
688
+ trades.append(trade)
689
+
690
+ # Sort trades by created_at with the most recent first
691
+ trades = sorted(
692
+ trades,
693
+ key=lambda x: x.updated_at,
694
+ reverse=True
695
+ )
696
+ queue = PeekableQueue(trades)
697
+
698
+ while difference != 0 and not queue.is_empty():
699
+ trade = queue.dequeue()
700
+ trade.available_amount -= difference
701
+ self.save(trade)
702
+
703
+ def update_trades_with_market_data(self, market_data):
704
+ """
705
+ Function to update trades with market data. This function will
706
+ update the last reported price and last reported price date of the
707
+ trade.
708
+
709
+ Args:
710
+ market_data: dict representing the market data
711
+ that will be used to update the trades
712
+
713
+ Returns:
714
+ None
715
+ """
716
+ open_trades = self.get_all({"status": TradeStatus.OPEN.value})
717
+ meta_data = market_data["metadata"]
718
+
719
+ for open_trade in open_trades:
720
+ ohlcv_meta_data = meta_data[DataType.OHLCV]
721
+
722
+ if open_trade.symbol not in ohlcv_meta_data:
723
+ continue
724
+
725
+ timeframes = ohlcv_meta_data[open_trade.symbol].keys()
726
+ sorted_timeframes = sorted(timeframes)
727
+ most_granular_interval = sorted_timeframes[0]
728
+ identifier = (
729
+ ohlcv_meta_data[open_trade.symbol][most_granular_interval]
730
+ )
731
+ data = market_data[identifier]
732
+
733
+ # Get last row of data
734
+ last_row = data.tail(1)
735
+ update_data = {
736
+ "last_reported_price": last_row["Close"][0],
737
+ "last_reported_price_datetime": last_row["Datetime"][0],
738
+ "updated_at": last_row["Datetime"][0]
739
+ }
740
+ self.update(open_trade.id, update_data)
741
+
742
+ def add_stop_loss(
743
+ self,
744
+ trade,
745
+ percentage: float,
746
+ trailing: bool = False,
747
+ sell_percentage: float = 100,
748
+ created_at: datetime = None
749
+ ) -> TradeStopLoss:
750
+ """
751
+ Function to add a stop loss to a trade.
752
+
753
+ Example of fixed stop loss:
754
+ * You buy BTC at $40,000.
755
+ * You set a SL of 5% → SL level at $38,000 (40,000 - 5%).
756
+ * BTC price increases to $42,000 → SL level remains at $38,000.
757
+ * BTC price drops to $38,000 → SL level reached, trade closes.
758
+
759
+ Example of trailing stop loss:
760
+ * You buy BTC at $40,000.
761
+ * You set a TSL of 5%, setting the sell price at $38,000.
762
+ * BTC price increases to $42,000 → New TSL level at
763
+ $39,900 (42,000 - 5%).
764
+ * BTC price drops to $39,900 → SL level reached, trade closes.
765
+
766
+ Args:
767
+ trade: Trade object representing the trade
768
+ percentage: float representing the percentage of the open price
769
+ that the stop loss should be set at
770
+ trailing (bool): representing whether the stop loss is a
771
+ trailing stop loss or not. Default is False.
772
+ sell_percentage: float representing the percentage of the trade
773
+ that should be sold if the stop loss is triggered.
774
+ created_at: datetime representing the creation date of the
775
+ stop loss. If None, the current datetime will be used.
776
+
777
+ Returns:
778
+ None
779
+ """
780
+ trade = self.get(trade.id)
781
+
782
+ # Check if the sell percentage + the existing stop losses is
783
+ # greater than 100
784
+ existing_sell_percentage = 0
785
+ for stop_loss in trade.stop_losses:
786
+ existing_sell_percentage += stop_loss.sell_percentage
787
+
788
+ if existing_sell_percentage + sell_percentage > 100:
789
+ raise OperationalException(
790
+ "Combined sell percentages of stop losses belonging "
791
+ "to trade exceeds 100."
792
+ )
793
+
794
+ creation_data = {
795
+ "trade_id": trade.id,
796
+ "trailing": trailing,
797
+ "percentage": percentage,
798
+ "open_price": trade.open_price,
799
+ "total_amount_trade": trade.amount,
800
+ "sell_percentage": sell_percentage,
801
+ "active": True,
802
+ "created_at": created_at if created_at is not None
803
+ else datetime.now(tz=timezone.utc)
804
+ }
805
+ return self.trade_stop_loss_repository.create(creation_data)
806
+
807
+ def add_take_profit(
808
+ self,
809
+ trade,
810
+ percentage: float,
811
+ trailing: bool = False,
812
+ sell_percentage: float = 100,
813
+ created_at: datetime = None
814
+ ) -> TradeTakeProfit:
815
+ """
816
+ Function to add a take profit to a trade. This function will add a
817
+ take profit to the specified trade. If the take profit is triggered,
818
+ the trade will be closed.
819
+
820
+ Example of take profit:
821
+ * You buy BTC at $40,000.
822
+ * You set a TP of 5% → TP level at $42,000 (40,000 + 5%).
823
+ * BTC rises to $42,000 → TP level reached, trade
824
+ closes, securing profit.
825
+
826
+ Example of trailing take profit:
827
+ * You buy BTC at $40,000
828
+ * You set a TTP of 5%, setting the sell price at $42,000.
829
+ * BTC rises to $42,000 → TTP level stays at $42,000.
830
+ * BTC rises to $45,000 → New TTP level at $42,750.
831
+ * BTC drops to $42,750 → Trade closes, securing profit.
832
+
833
+ Args:
834
+ trade: Trade object representing the trade
835
+ percentage (float): representing the percentage of the open price
836
+ that the stop loss should be set at. This must be a positive
837
+ number, e.g. 5 for 5%, or 10 for 10%.
838
+ trailing (bool): representing whether the take profit is a
839
+ trailing take profit or not. Default is False.
840
+ sell_percentage (float): representing the percentage of the trade
841
+ that should be sold if the stop loss is triggered
842
+ created_at (datetime): datetime representing the creation
843
+ date of the take profit. If None, the current datetime
844
+ will be used.
845
+
846
+ Returns:
847
+ None
848
+ """
849
+ trade = self.get(trade.id)
850
+
851
+ # Check if the sell percentage + the existing stop losses is
852
+ # greater than 100
853
+ existing_sell_percentage = 0
854
+ for take_profit in trade.take_profits:
855
+ existing_sell_percentage += take_profit.sell_percentage
856
+
857
+ if existing_sell_percentage + sell_percentage > 100:
858
+ raise OperationalException(
859
+ "Combined sell percentages of stop losses belonging "
860
+ "to trade exceeds 100."
861
+ )
862
+ creation_data = {
863
+ "trade_id": trade.id,
864
+ "trailing": trailing,
865
+ "percentage": percentage,
866
+ "open_price": trade.open_price,
867
+ "total_amount_trade": trade.amount,
868
+ "sell_percentage": sell_percentage,
869
+ "active": True,
870
+ "created_at": created_at if created_at is not None
871
+ else datetime.now(tz=timezone.utc)
872
+ }
873
+ return self.trade_take_profit_repository.create(creation_data)
874
+
875
+ def get_triggered_stop_loss_orders(self):
876
+ """
877
+ Function to get all triggered stop loss orders. This function will
878
+ return a list of trade ids that have triggered stop losses.
879
+
880
+ Returns:
881
+ List of trade ids
882
+ """
883
+ sell_orders_data = []
884
+ query = {"status": TradeStatus.OPEN.value}
885
+ open_trades = self.get_all(query)
886
+ to_be_saved_stop_loss_objects = []
887
+
888
+ # Group trades by target symbol
889
+ stop_losses_by_target_symbol = {}
890
+
891
+ for open_trade in open_trades:
892
+ triggered_stop_losses = []
893
+
894
+ for stop_loss in open_trade.stop_losses:
895
+
896
+ if (
897
+ stop_loss.active
898
+ and stop_loss.has_triggered(open_trade.last_reported_price)
899
+ ):
900
+ triggered_stop_losses.append(stop_loss)
901
+
902
+ to_be_saved_stop_loss_objects.append(stop_loss)
903
+
904
+ if len(triggered_stop_losses) > 0:
905
+ stop_losses_by_target_symbol[open_trade] = \
906
+ triggered_stop_losses
907
+
908
+ for trade in stop_losses_by_target_symbol:
909
+ stop_losses = stop_losses_by_target_symbol[trade]
910
+ available_amount = trade.available_amount
911
+ stop_loss_que = PeekableQueue(stop_losses)
912
+ order_amount = 0
913
+ stop_loss_metadata = []
914
+
915
+ # While there is an available amount and there are stop losses
916
+ # to process
917
+ while not stop_loss_que.is_empty() and available_amount > 0:
918
+ stop_loss = stop_loss_que.dequeue()
919
+ stop_loss_sell_amount = stop_loss.get_sell_amount()
920
+
921
+ if stop_loss_sell_amount <= available_amount:
922
+ available_amount = available_amount - stop_loss_sell_amount
923
+ stop_loss.active = False
924
+ stop_loss.sold_amount += stop_loss_sell_amount
925
+ order_amount += stop_loss_sell_amount
926
+ else:
927
+ stop_loss.sold_amount += available_amount
928
+
929
+ # Deactivate stop loss if the filled amount is equal
930
+ # to the amount of the trade, meaning that there is
931
+ # nothing left to sell
932
+ if trade.filled_amount == trade.amount:
933
+ stop_loss.active = False
934
+ else:
935
+ stop_loss.active = True
936
+
937
+ order_amount += available_amount
938
+ stop_loss_sell_amount = available_amount
939
+ available_amount = 0
940
+
941
+ stop_loss_metadata.append({
942
+ "stop_loss_id": stop_loss.id,
943
+ "amount": stop_loss_sell_amount
944
+ })
945
+ stop_loss.add_sell_price(
946
+ trade.last_reported_price,
947
+ trade.last_reported_price_datetime
948
+ )
949
+
950
+ position = self.position_repository.find({
951
+ "order_id": trade.orders[0].id
952
+ })
953
+ portfolio_id = position.portfolio_id
954
+ sell_orders_data.append(
955
+ {
956
+ "target_symbol": trade.target_symbol,
957
+ "trading_symbol": trade.trading_symbol,
958
+ "amount": order_amount,
959
+ "price": trade.last_reported_price,
960
+ "order_type": OrderType.LIMIT.value,
961
+ "order_side": OrderSide.SELL.value,
962
+ "portfolio_id": portfolio_id,
963
+ "stop_losses": stop_loss_metadata,
964
+ "trades": [{
965
+ "trade_id": trade.id,
966
+ "amount": order_amount
967
+ }]
968
+ }
969
+ )
970
+
971
+ self.trade_stop_loss_repository\
972
+ .save_objects(to_be_saved_stop_loss_objects)
973
+ return sell_orders_data
974
+
975
+ def get_triggered_take_profit_orders(self):
976
+ """
977
+ Function to get all triggered stop loss orders. This function will
978
+ return a list of trade ids that have triggered stop losses.
979
+
980
+ Returns:
981
+ List of trade objects. A trade object is a dictionary
982
+ """
983
+ sell_orders_data = []
984
+ query = {"status": TradeStatus.OPEN.value}
985
+ open_trades = self.get_all(query)
986
+ to_be_saved_take_profit_objects = []
987
+
988
+ # Group trades by target symbol
989
+ take_profits_by_target_symbol = {}
990
+
991
+ for open_trade in open_trades:
992
+ triggered_take_profits = []
993
+ available_amount = open_trade.available_amount
994
+
995
+ # Skip if there is no available amount
996
+ if available_amount == 0:
997
+ continue
998
+
999
+ for take_profit in open_trade.take_profits:
1000
+ if (
1001
+ take_profit.active and
1002
+ take_profit.has_triggered(open_trade.last_reported_price)
1003
+ ):
1004
+ triggered_take_profits.append(take_profit)
1005
+
1006
+ to_be_saved_take_profit_objects.append(take_profit)
1007
+
1008
+ if len(triggered_take_profits) > 0:
1009
+ take_profits_by_target_symbol[open_trade] = \
1010
+ triggered_take_profits
1011
+
1012
+ for trade in take_profits_by_target_symbol:
1013
+ take_profits = take_profits_by_target_symbol[trade]
1014
+ available_amount = trade.available_amount
1015
+ take_profit_que = PeekableQueue(take_profits)
1016
+ order_amount = 0
1017
+ take_profit_metadata = []
1018
+
1019
+ # While there is an available amount and there are take profits
1020
+ # to process
1021
+ while not take_profit_que.is_empty() and available_amount > 0:
1022
+ take_profit = take_profit_que.dequeue()
1023
+ take_profit_sell_amount = take_profit.get_sell_amount()
1024
+
1025
+ if take_profit_sell_amount <= available_amount:
1026
+ available_amount = available_amount - \
1027
+ take_profit_sell_amount
1028
+ take_profit.active = False
1029
+ take_profit.sold_amount += take_profit_sell_amount
1030
+ order_amount += take_profit_sell_amount
1031
+ else:
1032
+ take_profit.sold_amount += available_amount
1033
+
1034
+ # Deactivate take profit if the filled amount is equal
1035
+ # to the amount of the trade, meaning that there is
1036
+ # nothing left to sell
1037
+ if trade.filled_amount == trade.amount:
1038
+ take_profit.active = False
1039
+ else:
1040
+ take_profit.active = True
1041
+
1042
+ order_amount += available_amount
1043
+ take_profit_sell_amount = available_amount
1044
+ available_amount = 0
1045
+
1046
+ take_profit_metadata.append({
1047
+ "take_profit_id": take_profit.id,
1048
+ "amount": take_profit_sell_amount
1049
+ })
1050
+
1051
+ take_profit.add_sell_price(
1052
+ trade.last_reported_price,
1053
+ trade.last_reported_price_datetime
1054
+ )
1055
+
1056
+ position = self.position_repository.find({
1057
+ "order_id": trade.orders[0].id
1058
+ })
1059
+ portfolio_id = position.portfolio_id
1060
+ sell_orders_data.append(
1061
+ {
1062
+ "target_symbol": trade.target_symbol,
1063
+ "trading_symbol": trade.trading_symbol,
1064
+ "amount": order_amount,
1065
+ "price": trade.last_reported_price,
1066
+ "order_type": OrderType.LIMIT.value,
1067
+ "order_side": OrderSide.SELL.value,
1068
+ "portfolio_id": portfolio_id,
1069
+ "take_profits": take_profit_metadata,
1070
+ "trades": [{
1071
+ "trade_id": trade.id,
1072
+ "amount": order_amount
1073
+ }]
1074
+ }
1075
+ )
1076
+
1077
+ self.trade_take_profit_repository\
1078
+ .save_objects(to_be_saved_take_profit_objects)
1079
+ return sell_orders_data
1080
+
1081
+ def _create_order_id(self) -> str:
1082
+ """
1083
+ Function to create a unique order id. This function will
1084
+ create a unique order id based on the current time and
1085
+ the order id counter.
1086
+
1087
+ Returns:
1088
+ str: Unique order id
1089
+ """
1090
+ unique = False
1091
+ order_id = None
1092
+
1093
+ while not unique:
1094
+ order_id = f"{random_number(8)}-{random_string(8)}"
1095
+
1096
+ if not self.exists({"order_id": order_id}):
1097
+ unique = True
1098
+
1099
+ return order_id