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,1724 @@
1
+ import logging
2
+ from datetime import datetime, timezone
3
+ from typing import List
4
+
5
+ from investing_algorithm_framework.services import ConfigurationService, \
6
+ MarketCredentialService, OrderService, PortfolioConfigurationService, \
7
+ PortfolioService, PositionService, TradeService, DataProviderService, \
8
+ TradeStopLossService, TradeTakeProfitService
9
+ from investing_algorithm_framework.domain import OrderStatus, OrderType, \
10
+ OrderSide, OperationalException, Portfolio, RoundingService, \
11
+ BACKTESTING_FLAG, INDEX_DATETIME, Order, \
12
+ Position, Trade, TradeStatus, MarketCredential, TradeStopLoss, \
13
+ TradeTakeProfit
14
+
15
+ logger = logging.getLogger("investing_algorithm_framework")
16
+
17
+
18
+ class Context:
19
+ """
20
+ Context class to store the state of the algorithm and
21
+ give access to objects such as orders, positions, trades and
22
+ portfolio.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ configuration_service: ConfigurationService,
28
+ portfolio_configuration_service: PortfolioConfigurationService,
29
+ portfolio_service: PortfolioService,
30
+ position_service: PositionService,
31
+ order_service: OrderService,
32
+ market_credential_service: MarketCredentialService,
33
+ trade_service: TradeService,
34
+ trade_stop_loss_service: TradeStopLossService,
35
+ trade_take_profit_service: TradeTakeProfitService,
36
+ data_provider_service: DataProviderService
37
+ ):
38
+ self.configuration_service: ConfigurationService = \
39
+ configuration_service
40
+ self.portfolio_configuration_service: PortfolioConfigurationService = \
41
+ portfolio_configuration_service
42
+ self.portfolio_service: PortfolioService = portfolio_service
43
+ self.position_service: PositionService = position_service
44
+ self.order_service: OrderService = order_service
45
+ self.market_credential_service: MarketCredentialService = \
46
+ market_credential_service
47
+ self.data_provider_service: DataProviderService = data_provider_service
48
+ self.trade_service: TradeService = trade_service
49
+ self.trade_stop_loss_service: TradeStopLossService = \
50
+ trade_stop_loss_service
51
+ self.trade_take_profit_service: TradeTakeProfitService = \
52
+ trade_take_profit_service
53
+
54
+ @property
55
+ def config(self):
56
+ """
57
+ Function to get a config instance. This allows users when
58
+ having access to the algorithm instance also to read the
59
+ configs of the app.
60
+ """
61
+ return self.configuration_service.get_config()
62
+
63
+ def get_config(self):
64
+ """
65
+ Function to get a config instance. This allows users when
66
+ having access to the algorithm instance also to read the
67
+ configs of the app.
68
+ """
69
+ return self.configuration_service.get_config()
70
+
71
+ def create_order(
72
+ self,
73
+ target_symbol,
74
+ price,
75
+ order_type,
76
+ order_side,
77
+ amount,
78
+ market=None,
79
+ execute=True,
80
+ validate=True,
81
+ sync=True
82
+ ) -> Order:
83
+ """
84
+ Function to create an order. This function will create an order
85
+ and execute it if the execute parameter is set to True. If the
86
+ validate parameter is set to True, the order will be validated
87
+
88
+ Args:
89
+ target_symbol: The symbol of the asset to trade
90
+ price: The price of the asset
91
+ order_type: The type of the order
92
+ order_side: The side of the order
93
+ amount: The amount of the asset to trade
94
+ market: The market to trade the asset
95
+ execute: If set to True, the order will be executed
96
+ validate: If set to True, the order will be validated
97
+ sync: If set to True, the created order will be synced
98
+ with the portfolio of the algorithm.
99
+
100
+ Returns:
101
+ The order created
102
+ """
103
+ portfolio = self.portfolio_service.find({"market": market})
104
+ order_data = {
105
+ "target_symbol": target_symbol,
106
+ "price": price,
107
+ "amount": amount,
108
+ "order_type": order_type,
109
+ "order_side": order_side,
110
+ "portfolio_id": portfolio.id,
111
+ "status": OrderStatus.CREATED.value,
112
+ "trading_symbol": portfolio.trading_symbol,
113
+ }
114
+
115
+ if BACKTESTING_FLAG in self.configuration_service.config \
116
+ and self.configuration_service.config[BACKTESTING_FLAG]:
117
+ order_data["created_at"] = \
118
+ self.configuration_service.config[INDEX_DATETIME]
119
+
120
+ return self.order_service.create(
121
+ order_data, execute=execute, validate=validate, sync=sync
122
+ )
123
+
124
+ def has_balance(self, symbol, amount, market=None):
125
+ """
126
+ Function to check if the portfolio has enough balance to
127
+ create an order. This function will return True if the
128
+ portfolio has enough balance to create an order, False
129
+ otherwise.
130
+
131
+ Parameters:
132
+ symbol: The symbol of the asset
133
+ amount: The amount of the asset
134
+ market: The market of the asset
135
+
136
+ Returns:
137
+ Boolean: True if the portfolio has enough balance
138
+ """
139
+
140
+ portfolio = self.portfolio_service.find({"market": market})
141
+ position = self.position_service.find(
142
+ {"portfolio": portfolio.id, "symbol": symbol}
143
+ )
144
+
145
+ if position is None:
146
+ return False
147
+
148
+ return position.get_amount() >= amount
149
+
150
+ def create_limit_order(
151
+ self,
152
+ target_symbol,
153
+ price,
154
+ order_side,
155
+ amount=None,
156
+ amount_trading_symbol=None,
157
+ percentage=None,
158
+ percentage_of_portfolio=None,
159
+ percentage_of_position=None,
160
+ precision=None,
161
+ market=None,
162
+ execute=True,
163
+ validate=True,
164
+ sync=True
165
+ ) -> Order:
166
+ """
167
+ Function to create a limit order. This function will create a limit
168
+ order and execute it if the execute parameter is set to True. If the
169
+ validate parameter is set to True, the order will be validated
170
+
171
+ Args:
172
+ target_symbol: The symbol of the asset to trade
173
+ price: The price of the asset
174
+ order_side: The side of the order
175
+ amount (optional): The amount of the asset to trade
176
+ amount_trading_symbol (optional): The amount of the
177
+ trading symbol to trade
178
+ percentage (optional): The percentage of the portfolio
179
+ to allocate to the
180
+ order
181
+ percentage_of_portfolio (optional): The percentage
182
+ of the portfolio to allocate to the order
183
+ percentage_of_position (optional): The percentage
184
+ of the position to allocate to
185
+ the order. (Only supported for SELL orders)
186
+ precision (optional): The precision of the amount
187
+ market (optional): The market to trade the asset
188
+ execute (optional): Default True. If set to True,
189
+ the order will be executed
190
+ validate (optional): Default True. If set to
191
+ True, the order will be validated
192
+ sync (optional): Default True. If set to True,
193
+ the created order will be synced with the
194
+ portfolio of the algorithm
195
+
196
+ Returns:
197
+ Order: Instance of the order created
198
+ """
199
+ portfolio = self.portfolio_service.find({"market": market})
200
+
201
+ if percentage_of_portfolio is not None:
202
+ if not OrderSide.BUY.equals(order_side):
203
+ raise OperationalException(
204
+ "Percentage of portfolio is only supported for BUY orders."
205
+ )
206
+
207
+ net_size = portfolio.get_net_size()
208
+ size = net_size * (percentage_of_portfolio / 100)
209
+ amount = size / price
210
+
211
+ elif percentage_of_position is not None:
212
+
213
+ if not OrderSide.SELL.equals(order_side):
214
+ raise OperationalException(
215
+ "Percentage of position is only supported for SELL orders."
216
+ )
217
+
218
+ position = self.position_service.find(
219
+ {
220
+ "symbol": target_symbol,
221
+ "portfolio": portfolio.id
222
+ }
223
+ )
224
+ amount = position.get_amount() * (percentage_of_position / 100)
225
+
226
+ elif percentage is not None:
227
+ net_size = portfolio.get_net_size()
228
+ size = net_size * (percentage / 100)
229
+ amount = size / price
230
+
231
+ if precision is not None:
232
+ amount = RoundingService.round_down(amount, precision)
233
+
234
+ if amount_trading_symbol is not None:
235
+ amount = amount_trading_symbol / price
236
+
237
+ if amount is None:
238
+ raise OperationalException(
239
+ "The amount parameter is required to create a limit order." +
240
+ "Either the amount, amount_trading_symbol, percentage, " +
241
+ "percentage_of_portfolio or percentage_of_position "
242
+ "parameter must be specified."
243
+ )
244
+
245
+ logger.info(
246
+ f"Creating limit order: {target_symbol} "
247
+ f"{order_side} {amount} @ {price}"
248
+ )
249
+ order_data = {
250
+ "target_symbol": target_symbol,
251
+ "price": price,
252
+ "amount": amount,
253
+ "order_type": OrderType.LIMIT.value,
254
+ "order_side": OrderSide.from_value(order_side).value,
255
+ "portfolio_id": portfolio.id,
256
+ "status": OrderStatus.CREATED.value,
257
+ "trading_symbol": portfolio.trading_symbol,
258
+ }
259
+
260
+ if BACKTESTING_FLAG in self.configuration_service.config \
261
+ and self.configuration_service.config[BACKTESTING_FLAG]:
262
+ order_data["created_at"] = \
263
+ self.configuration_service.config[INDEX_DATETIME]
264
+
265
+ return self.order_service.create(
266
+ order_data, execute=execute, validate=validate, sync=sync
267
+ )
268
+
269
+ def create_limit_sell_order(
270
+ self,
271
+ target_symbol,
272
+ price,
273
+ amount=None,
274
+ percentage_of_position=None,
275
+ market=None,
276
+ portfolio_id=None
277
+ ) -> Order:
278
+ """
279
+ Function to create a limit sell order. This function will create
280
+ a limit sell order. If the amount parameter is specified, the
281
+ order will be created with the specified amount. If the
282
+ percentage_of_position parameter is specified, the order will be
283
+ created with the percentage of the position specified. If neither
284
+ the amount nor the percentage_of_position parameter is specified,
285
+ an OperationalException will be raised.
286
+
287
+ Args:
288
+ target_symbol (str): The symbol of the asset to sell
289
+ price (float): The price at which to sell the asset
290
+ amount (float, optional): The amount of the asset to sell
291
+ percentage_of_position (float, optional): The percentage of the
292
+ position to sell.
293
+ market (str, optional): the portfolio corresponding to the market
294
+ to sell the asset
295
+ portfolio_id: (str, optional): The ID of the portfolio to sell
296
+ the asset from.
297
+
298
+ Returns:
299
+ Order: The order created
300
+ """
301
+ if amount is None and percentage_of_position is None:
302
+ raise OperationalException(
303
+ "Either amount or percentage_of_position must be specified "
304
+ "to create a limit sell order."
305
+ )
306
+
307
+ if portfolio_id is not None:
308
+ portfolio = self.portfolio_service.get(portfolio_id)
309
+ elif market is not None:
310
+ portfolio = self.portfolio_service.find({"market": market})
311
+ else:
312
+ portfolio = self.portfolio_service.get_all()[0]
313
+
314
+ if percentage_of_position is not None:
315
+ position = self.position_service.find(
316
+ {
317
+ "symbol": target_symbol,
318
+ "portfolio": portfolio.id
319
+ }
320
+ )
321
+ amount = position.get_amount() * (percentage_of_position / 100)
322
+
323
+ logger.info(
324
+ f"Creating limit order: {target_symbol} "
325
+ f"SELL {amount} @ {price}"
326
+ )
327
+ order_data = {
328
+ "target_symbol": target_symbol,
329
+ "price": price,
330
+ "amount": amount,
331
+ "order_type": OrderType.LIMIT.value,
332
+ "order_side": OrderSide.SELL.value,
333
+ "portfolio_id": portfolio.id,
334
+ "status": OrderStatus.CREATED.value,
335
+ "trading_symbol": portfolio.trading_symbol,
336
+ }
337
+
338
+ if BACKTESTING_FLAG in self.configuration_service.config \
339
+ and self.configuration_service.config[BACKTESTING_FLAG]:
340
+ order_data["created_at"] = \
341
+ self.configuration_service.config[INDEX_DATETIME]
342
+
343
+ return self.order_service.create(order_data)
344
+
345
+ def create_limit_buy_order(
346
+ self,
347
+ target_symbol,
348
+ price,
349
+ amount=None,
350
+ percentage_of_portfolio=None,
351
+ market=None,
352
+ portfolio_id=None
353
+ ) -> Order:
354
+ """
355
+ Function to create a limit buy order. This function will create
356
+ a limit buy order. If the amount parameter is specified, the
357
+ order will be created with the specified amount. If the
358
+ percentage_of_portfolio parameter is specified, the order will be
359
+ created with the percentage of the portfolio specified. If neither
360
+ the amount nor the percentage_of_portfolio parameter is specified,
361
+ an OperationalException will be raised.
362
+
363
+ Args:
364
+ target_symbol (str): The symbol of the asset to buy
365
+ price (float): The price at which to buy the asset
366
+ amount (float, optional): The amount of the asset to buy
367
+ percentage_of_portfolio (float, optional): The percentage of the
368
+ portfolio to buy.
369
+ market (str, optional): the portfolio corresponding to the market
370
+ to buy the asset
371
+ portfolio_id (str, optional): The ID of the portfolio to buy
372
+ the asset from.
373
+
374
+ Returns:
375
+ Order: The order created
376
+ """
377
+
378
+ if amount is None and percentage_of_portfolio is None:
379
+ raise OperationalException(
380
+ "Either amount or percentage_of_portfolio must be specified "
381
+ "to create a limit buy order."
382
+ )
383
+
384
+ if portfolio_id is not None:
385
+ portfolio = self.portfolio_service.get(portfolio_id)
386
+ elif market is not None:
387
+ portfolio = self.portfolio_service.find({"market": market})
388
+ else:
389
+ portfolio = self.portfolio_service.get_all()[0]
390
+
391
+ if percentage_of_portfolio is not None:
392
+ net_size = portfolio.get_net_size()
393
+ size = net_size * (percentage_of_portfolio / 100)
394
+ amount = size / price
395
+ logger.info(
396
+ f"Creating limit order: {target_symbol} "
397
+ f"BUY {amount} @ {price}"
398
+ )
399
+ order_data = {
400
+ "target_symbol": target_symbol,
401
+ "price": price,
402
+ "amount": amount,
403
+ "order_type": OrderType.LIMIT.value,
404
+ "order_side": OrderSide.BUY.value,
405
+ "portfolio_id": portfolio.id,
406
+ "status": OrderStatus.CREATED.value,
407
+ "trading_symbol": portfolio.trading_symbol,
408
+ }
409
+ if BACKTESTING_FLAG in self.configuration_service.config \
410
+ and self.configuration_service.config[BACKTESTING_FLAG]:
411
+ order_data["created_at"] = \
412
+ self.configuration_service.config[INDEX_DATETIME]
413
+ return self.order_service.create(
414
+ order_data, execute=True, validate=True, sync=True
415
+ )
416
+
417
+ def get_portfolio(self, market=None) -> Portfolio:
418
+ """
419
+ Function to get the portfolio of the algorithm. This function
420
+ will return the portfolio of the algorithm. If the market
421
+ parameter is specified, the portfolio of the specified market
422
+ will be returned.
423
+
424
+ Parameters:
425
+ market: The market of the portfolio
426
+
427
+ Returns:
428
+ Portfolio: The portfolio of the algorithm
429
+ """
430
+
431
+ if market is None:
432
+ portfolio = self.portfolio_service.get_all()[0]
433
+ else:
434
+ portfolio = self.portfolio_service.find({"market": market})
435
+
436
+ # Retrieve positions
437
+ positions = self.position_service.get_all(
438
+ {"portfolio": portfolio.id}
439
+ )
440
+
441
+ if BACKTESTING_FLAG in self.configuration_service.config \
442
+ and self.configuration_service.config[BACKTESTING_FLAG]:
443
+ date = self.configuration_service.config[INDEX_DATETIME]
444
+ else:
445
+ date = datetime.now(tz=timezone.utc)
446
+
447
+ allocated = 0.0
448
+
449
+ for position in positions:
450
+
451
+ if position.symbol != portfolio.trading_symbol:
452
+ ticker = self.data_provider_service.get_ticker_data(
453
+ symbol=f"{position.symbol}/{portfolio.trading_symbol}",
454
+ market=portfolio.market,
455
+ date=date
456
+ )
457
+ if ticker is not None and "bid" in ticker:
458
+ allocated += position.get_amount() * ticker["bid"]
459
+
460
+ portfolio.allocated = allocated
461
+ return portfolio
462
+
463
+ def get_latest_price(self, symbol, market=None):
464
+
465
+ if BACKTESTING_FLAG in self.configuration_service.config \
466
+ and self.configuration_service.config[BACKTESTING_FLAG]:
467
+ date = self.configuration_service.config[INDEX_DATETIME]
468
+ else:
469
+ date = datetime.now(tz=timezone.utc)
470
+
471
+ ticker = self.data_provider_service.get_ticker_data(
472
+ symbol=symbol,
473
+ market=market,
474
+ date=date
475
+ )
476
+
477
+ return ticker['bid'] if ticker and 'bid' in ticker else None
478
+
479
+ def get_portfolios(self):
480
+ """
481
+ Function to get all portfolios of the algorithm. This function
482
+ will return all portfolios of the algorithm.
483
+
484
+ Returns:
485
+ List[Portfolio]: A list of all portfolios of the algorithm
486
+ """
487
+ return self.portfolio_service.get_all()
488
+
489
+ def get_unallocated(self, market=None) -> float:
490
+ """
491
+ Function to get the unallocated balance of the portfolio. This
492
+ function will return the unallocated balance of the portfolio.
493
+ If the market parameter is specified, the unallocated balance
494
+ of the specified market will be returned.
495
+
496
+ Args:
497
+ market: The market of the portfolio
498
+
499
+ Returns:
500
+ float: The unallocated balance of the portfolio
501
+ """
502
+
503
+ if market:
504
+ portfolio = self.portfolio_service.find({{"market": market}})
505
+ else:
506
+ portfolio = self.portfolio_service.get_all()[0]
507
+
508
+ trading_symbol = portfolio.trading_symbol
509
+ return self.position_service.find(
510
+ {"portfolio": portfolio.id, "symbol": trading_symbol}
511
+ ).get_amount()
512
+
513
+ def get_total_size(self):
514
+ """
515
+ Returns the total size of the portfolio.
516
+
517
+ The total size of the portfolio is the unallocated balance and the
518
+ allocated balance of the portfolio.
519
+
520
+ Returns:
521
+ float: The total size of the portfolio
522
+ """
523
+ return self.get_unallocated() + self.get_allocated()
524
+
525
+ def get_order(
526
+ self,
527
+ reference_id=None,
528
+ market=None,
529
+ target_symbol=None,
530
+ trading_symbol=None,
531
+ order_side=None,
532
+ order_type=None
533
+ ) -> Order:
534
+ """
535
+ Function to retrieve an order.
536
+
537
+ Exception is thrown when no param has been provided.
538
+
539
+ Args:
540
+ reference_id [optional] (int): id given by the external
541
+ market or exchange.
542
+ market [optional] (str): the market that the order was
543
+ executed on.
544
+ target_symbol [optional] (str): the symbol of the asset
545
+ that the order was executed
546
+ """
547
+ query_params = {}
548
+
549
+ if reference_id:
550
+ query_params["reference_id"] = reference_id
551
+
552
+ if target_symbol:
553
+ query_params["target_symbol"] = target_symbol
554
+
555
+ if trading_symbol:
556
+ query_params["trading_symbol"] = trading_symbol
557
+
558
+ if order_side:
559
+ query_params["order_side"] = order_side
560
+
561
+ if order_type:
562
+ query_params["order_type"] = order_type
563
+
564
+ if market:
565
+ portfolio = self.portfolio_service.find({"market": market})
566
+ positions = self.position_service.get_all(
567
+ {"portfolio": portfolio.id}
568
+ )
569
+ query_params["position"] = [position.id for position in positions]
570
+
571
+ if not query_params:
572
+ raise OperationalException(
573
+ "No parameters provided to get order."
574
+ )
575
+
576
+ return self.order_service.find(query_params)
577
+
578
+ def get_orders(
579
+ self,
580
+ target_symbol=None,
581
+ status=None,
582
+ order_type=None,
583
+ order_side=None,
584
+ market=None
585
+ ) -> List[Order]:
586
+
587
+ if market is None:
588
+ portfolio = self.portfolio_service.get_all()[0]
589
+ else:
590
+ portfolio = self.portfolio_service.find({"market": market})
591
+
592
+ positions = self.position_service.get_all({"portfolio": portfolio.id})
593
+ return self.order_service.get_all(
594
+ {
595
+ "position": [position.id for position in positions],
596
+ "target_symbol": target_symbol,
597
+ "status": status,
598
+ "order_type": order_type,
599
+ "order_side": order_side
600
+ }
601
+ )
602
+
603
+ def get_positions(
604
+ self,
605
+ market=None,
606
+ identifier=None,
607
+ amount_gt=None,
608
+ amount_gte=None,
609
+ amount_lt=None,
610
+ amount_lte=None
611
+ ) -> List[Position]:
612
+ """
613
+ Function to get all positions. This function will return all
614
+ positions that match the specified query parameters. If the
615
+ market parameter is specified, the positions of the specified
616
+ market will be returned. If the identifier parameter is
617
+ specified, the positions of the specified portfolio will be
618
+ returned. If the amount_gt parameter is specified, the positions
619
+ with an amount greater than the specified amount will be returned.
620
+ If the amount_gte parameter is specified, the positions with an
621
+ amount greater than or equal to the specified amount will be
622
+ returned. If the amount_lt parameter is specified, the positions
623
+ with an amount less than the specified amount will be returned.
624
+ If the amount_lte parameter is specified, the positions with an
625
+ amount less than or equal to the specified amount will be returned.
626
+
627
+ Parameters:
628
+ market: The market of the portfolio where the positions are
629
+ identifier: The identifier of the portfolio
630
+ amount_gt: The amount of the asset must be greater than this
631
+ amount_gte: The amount of the asset must be greater than or
632
+ equal to this
633
+ amount_lt: The amount of the asset must be less than this
634
+ amount_lte: The amount of the asset must be less than or equal
635
+ to this
636
+
637
+ Returns:
638
+ List[Position]: A list of positions that match the query parameters
639
+ """
640
+ query_params = {}
641
+
642
+ if market is not None:
643
+ query_params["market"] = market
644
+
645
+ if identifier is not None:
646
+ query_params["identifier"] = identifier
647
+
648
+ if amount_gt is not None:
649
+ query_params["amount_gt"] = amount_gt
650
+
651
+ if amount_gte is not None:
652
+ query_params["amount_gte"] = amount_gte
653
+
654
+ if amount_lt is not None:
655
+ query_params["amount_lt"] = amount_lt
656
+
657
+ if amount_lte is not None:
658
+ query_params["amount_lte"] = amount_lte
659
+
660
+ portfolios = self.portfolio_service.get_all(query_params)
661
+
662
+ if not portfolios:
663
+ raise OperationalException("No portfolio found.")
664
+
665
+ portfolio = portfolios[0]
666
+ return self.position_service.get_all(
667
+ {"portfolio": portfolio.id}
668
+ )
669
+
670
+ def get_position(self, symbol, market=None, identifier=None) -> Position:
671
+ """
672
+ Function to get a position. This function will return the
673
+ position that matches the specified query parameters. If the
674
+ market parameter is specified, the position of the specified
675
+ market will be returned. If the identifier parameter is
676
+ specified, the position of the specified portfolio will be
677
+ returned.
678
+
679
+ Parameters:
680
+ symbol: The symbol of the asset that represents the position
681
+ market: The market of the portfolio where the position is located
682
+ identifier: The identifier of the portfolio
683
+
684
+ Returns:
685
+ Position: The position that matches the query parameters
686
+ """
687
+
688
+ query_params = {}
689
+
690
+ if market is not None:
691
+ query_params["market"] = market
692
+
693
+ if identifier is not None:
694
+ query_params["identifier"] = identifier
695
+
696
+ portfolios = self.portfolio_service.get_all(query_params)
697
+
698
+ if not portfolios:
699
+ raise OperationalException("No portfolio found.")
700
+
701
+ portfolio = portfolios[0]
702
+
703
+ try:
704
+ return self.position_service.find(
705
+ {"portfolio": portfolio.id, "symbol": symbol}
706
+ )
707
+ except OperationalException:
708
+ return None
709
+
710
+ def has_position(
711
+ self,
712
+ symbol,
713
+ market=None,
714
+ identifier=None,
715
+ amount_gt=0,
716
+ amount_gte=None,
717
+ amount_lt=None,
718
+ amount_lte=None
719
+ ):
720
+ """
721
+ Function to check if a position exists. This function will return
722
+ True if a position exists, False otherwise. This function will check
723
+ if the amount > 0 condition by default.
724
+
725
+ Parameters:
726
+ param symbol: The symbol of the asset
727
+ param market: The market of the asset
728
+ param identifier: The identifier of the portfolio
729
+ param amount_gt: The amount of the asset must be greater than this
730
+ param amount_gte: The amount of the asset must be greater than
731
+ or equal to this
732
+ param amount_lt: The amount of the asset must be less than this
733
+ param amount_lte: The amount of the asset must be less than
734
+ or equal to this
735
+
736
+ Returns:
737
+ Boolean: True if a position exists, False otherwise
738
+ """
739
+
740
+ return self.position_exists(
741
+ symbol=symbol,
742
+ market=market,
743
+ identifier=identifier,
744
+ amount_gt=amount_gt,
745
+ amount_gte=amount_gte,
746
+ amount_lt=amount_lt,
747
+ amount_lte=amount_lte
748
+ )
749
+
750
+ def position_exists(
751
+ self,
752
+ symbol,
753
+ market=None,
754
+ identifier=None,
755
+ amount_gt=None,
756
+ amount_gte=None,
757
+ amount_lt=None,
758
+ amount_lte=None
759
+ ) -> bool:
760
+ """
761
+ Function to check if a position exists. This function will return
762
+ True if a position exists, False otherwise. This function will
763
+ not check the amount > 0 condition by default. If you want to
764
+ check if a position exists with an amount greater than 0, you
765
+ can use the amount_gt parameter. If you want to check if a
766
+ position exists with an amount greater than or equal to a
767
+ certain amount, you can use the amount_gte parameter. If you
768
+ want to check if a position exists with an amount less than a
769
+ certain amount, you can use the amount_lt parameter. If you want
770
+ to check if a position exists with an amount less than or equal
771
+ to a certain amount, you can use the amount_lte parameter.
772
+
773
+ It is not recommended to use this method directly because it can
774
+ have adverse effects on the algorithm. It is recommended to use
775
+ the has_position method instead.
776
+
777
+ param symbol: The symbol of the asset
778
+ param market: The market of the asset
779
+ param identifier: The identifier of the portfolio
780
+ param amount_gt: The amount of the asset must be greater than this
781
+ param amount_gte: The amount of the asset must be greater than
782
+ or equal to this
783
+ param amount_lt: The amount of the asset must be less than this
784
+ param amount_lte: The amount of the asset must be less than
785
+ or equal to this
786
+
787
+ return: True if a position exists, False otherwise
788
+ """
789
+ query_params = {}
790
+
791
+ if market is not None:
792
+ query_params["market"] = market
793
+
794
+ if identifier is not None:
795
+ query_params["identifier"] = identifier
796
+
797
+ if amount_gt is not None:
798
+ query_params["amount_gt"] = amount_gt
799
+
800
+ if amount_gte is not None:
801
+ query_params["amount_gte"] = amount_gte
802
+
803
+ if amount_lt is not None:
804
+ query_params["amount_lt"] = amount_lt
805
+
806
+ if amount_lte is not None:
807
+ query_params["amount_lte"] = amount_lte
808
+
809
+ query_params["symbol"] = symbol
810
+ return self.position_service.exists(query_params)
811
+
812
+ def get_position_percentage_of_portfolio_by_net_size(
813
+ self, symbol, market=None, identifier=None
814
+ ) -> float:
815
+ """
816
+ Returns the percentage of the portfolio that is allocated to a
817
+ position. This is calculated by dividing the cost of the position
818
+ by the total net size of the portfolio.
819
+
820
+ The total net size of the portfolio is the initial balance of the
821
+ portfolio plus the all the net gains of your trades.
822
+ """
823
+ query_params = {}
824
+
825
+ if market is not None:
826
+ query_params["market"] = market
827
+
828
+ if identifier is not None:
829
+ query_params["identifier"] = identifier
830
+
831
+ portfolios = self.portfolio_service.get_all(query_params)
832
+
833
+ if not portfolios:
834
+ raise OperationalException("No portfolio found.")
835
+
836
+ portfolio = portfolios[0]
837
+ position = self.position_service.find(
838
+ {"portfolio": portfolio.id, "symbol": symbol}
839
+ )
840
+ net_size = portfolio.get_net_size()
841
+ return (position.cost / net_size) * 100
842
+
843
+ def close_position(
844
+ self,
845
+ position=None,
846
+ symbol=None,
847
+ portfolio=None,
848
+ precision=None,
849
+ price=None
850
+ ) -> Order:
851
+ """
852
+ Function to close a position. This function will close a position
853
+ by creating a market order to sell the position. If the precision
854
+ parameter is specified, the amount of the order will be rounded
855
+ down to the specified precision.
856
+
857
+ Args:
858
+ position (Optional): The position to close
859
+ symbol (Optional): The symbol of the asset
860
+ portfolio (Optional): The portfolio where the position is located
861
+ precision (Optional): The precision of the amount
862
+ price (Optional[Float]): The price with which the position needs
863
+ to be closed.
864
+
865
+ Returns:
866
+ Order: The order created to close the position
867
+ """
868
+ query_params = {}
869
+
870
+ if position is None and (symbol is None and portfolio is None):
871
+ raise OperationalException(
872
+ "Either position or symbol and portfolio parameters must "
873
+ "be specified to close a position."
874
+ )
875
+
876
+ if position is not None:
877
+ query_params["id"] = position.id
878
+ query_params["symbol"] = position.symbol
879
+
880
+ if symbol is not None:
881
+ query_params["symbol"] = symbol
882
+
883
+ if portfolio is not None:
884
+ query_params["portfolio"] = portfolio.id
885
+
886
+ position = self.position_service.find(query_params)
887
+ portfolio = self.portfolio_service.get(position.portfolio_id)
888
+
889
+ if position.get_amount() == 0:
890
+ logger.warning("Cannot close position. Amount is 0.")
891
+ return None
892
+
893
+ if position.get_symbol() == portfolio.get_trading_symbol():
894
+ raise OperationalException(
895
+ "Cannot close position. The position is the same as the "
896
+ "trading symbol of the portfolio."
897
+ )
898
+
899
+ for order in self.order_service \
900
+ .get_all(
901
+ {
902
+ "position": position.id,
903
+ "status": OrderStatus.OPEN.value
904
+ }
905
+ ):
906
+ self.order_service.cancel_order(order)
907
+
908
+ target_symbol = position.get_symbol()
909
+ symbol = f"{target_symbol.upper()}/{portfolio.trading_symbol.upper()}"
910
+
911
+ if price is None:
912
+ ticker = self.data_provider_service.get_ticker_data(
913
+ symbol=symbol,
914
+ market=portfolio.market,
915
+ date=self.config[INDEX_DATETIME]
916
+ )
917
+ price = ticker["bid"]
918
+
919
+ logger.info(
920
+ f"Closing position {position.symbol} "
921
+ f"with amount {position.get_amount()} "
922
+ f"at price {price}"
923
+ )
924
+ return self.create_limit_order(
925
+ target_symbol=position.symbol,
926
+ amount=position.get_amount(),
927
+ order_side=OrderSide.SELL.value,
928
+ price=price,
929
+ precision=precision,
930
+ )
931
+
932
+ def get_allocated(self, market=None, identifier=None) -> float:
933
+
934
+ if self.portfolio_configuration_service.count() > 1 \
935
+ and identifier is None and market is None:
936
+ raise OperationalException(
937
+ "Multiple portfolios found. Please specify a "
938
+ "portfolio identifier."
939
+ )
940
+
941
+ if market is not None and identifier is not None:
942
+ portfolio_configurations = self.portfolio_configuration_service \
943
+ .get_all()
944
+
945
+ else:
946
+ query_params = {"market": market, "identifier": identifier}
947
+ portfolio_configuration = self.portfolio_configuration_service \
948
+ .find(query_params)
949
+
950
+ if not portfolio_configuration:
951
+ raise OperationalException("No portfolio found.")
952
+
953
+ portfolio_configurations = [portfolio_configuration]
954
+
955
+ if len(portfolio_configurations) == 0:
956
+ raise OperationalException("No portfolio found.")
957
+
958
+ portfolios = []
959
+
960
+ for portfolio_configuration in portfolio_configurations:
961
+ portfolio = self.portfolio_service.find(
962
+ {"identifier": portfolio_configuration.identifier}
963
+ )
964
+ portfolio.configuration = portfolio_configuration
965
+ portfolios.append(portfolio)
966
+
967
+ allocated = 0
968
+
969
+ for portfolio in portfolios:
970
+ positions = self.position_service.get_all(
971
+ {"portfolio": portfolio.id}
972
+ )
973
+
974
+ for position in positions:
975
+ if portfolio.trading_symbol == position.symbol:
976
+ continue
977
+
978
+ symbol = f"{position.symbol.upper()}/" \
979
+ f"{portfolio.trading_symbol.upper()}"
980
+ current_date = self.config[INDEX_DATETIME]
981
+ ticker = self.data_provider_service.get_ticker_data(
982
+ symbol=symbol, market=portfolio.market, date=current_date
983
+ )
984
+ allocated = allocated + \
985
+ (position.get_amount() * ticker["bid"])
986
+
987
+ return allocated
988
+
989
+ def get_unfilled(self, market=None, identifier=None) -> float:
990
+
991
+ if self.portfolio_configuration_service.count() > 1 \
992
+ and identifier is None and market is None:
993
+ raise OperationalException(
994
+ "Multiple portfolios found. Please specify a "
995
+ "portfolio identifier."
996
+ )
997
+
998
+ if market is not None and identifier is not None:
999
+ portfolio_configurations = self.portfolio_configuration_service \
1000
+ .get_all()
1001
+
1002
+ else:
1003
+ query_params = {
1004
+ "market": market,
1005
+ "identifier": identifier
1006
+ }
1007
+ portfolio_configurations = [self.portfolio_configuration_service
1008
+ .find(query_params)]
1009
+
1010
+ portfolios = []
1011
+
1012
+ for portfolio_configuration in portfolio_configurations:
1013
+ portfolio = self.portfolio_service.find(
1014
+ {"identifier": portfolio_configuration.identifier}
1015
+ )
1016
+ portfolios.append(portfolio)
1017
+
1018
+ unfilled = 0
1019
+
1020
+ for portfolio in portfolios:
1021
+ orders = self.order_service.get_all(
1022
+ {"status": OrderStatus.OPEN.value, "portfolio": portfolio.id}
1023
+ )
1024
+ unfilled = unfilled + sum(
1025
+ [order.get_amount() * order.get_price() for order in orders]
1026
+ )
1027
+
1028
+ return unfilled
1029
+
1030
+ def get_portfolio_configurations(self):
1031
+ return self.portfolio_configuration_service.get_all()
1032
+
1033
+ def has_open_buy_orders(self, target_symbol, identifier=None, market=None):
1034
+ query_params = {}
1035
+
1036
+ if identifier is not None:
1037
+ portfolio = self.portfolio_service.find(
1038
+ {"identifier": identifier}
1039
+ )
1040
+ query_params["portfolio"] = portfolio.id
1041
+
1042
+ if market is not None:
1043
+ portfolio = self.portfolio_service.find(
1044
+ {"market": market}
1045
+ )
1046
+ query_params["portfolio"] = portfolio.id
1047
+
1048
+ query_params["target_symbol"] = target_symbol
1049
+ query_params["order_side"] = OrderSide.BUY.value
1050
+ query_params["status"] = OrderStatus.OPEN.value
1051
+ return self.order_service.exists(query_params)
1052
+
1053
+ def get_sell_orders(self, target_symbol, identifier=None, market=None):
1054
+ query_params = {}
1055
+
1056
+ if identifier is not None:
1057
+ portfolio = self.portfolio_service.find(
1058
+ {"identifier": identifier}
1059
+ )
1060
+ query_params["portfolio"] = portfolio.id
1061
+
1062
+ if market is not None:
1063
+ portfolio = self.portfolio_service.find(
1064
+ {"market": market}
1065
+ )
1066
+ query_params["portfolio"] = portfolio.id
1067
+
1068
+ query_params["target_symbol"] = target_symbol
1069
+ query_params["order_side"] = OrderSide.SELL.value
1070
+ return self.order_service.get_all(query_params)
1071
+
1072
+ def get_open_orders(
1073
+ self, target_symbol=None, identifier=None, market=None
1074
+ ) -> List[Order]:
1075
+ """
1076
+ Function to get all open orders. This function will return all
1077
+ open orders that match the specified query parameters.
1078
+
1079
+ Args:
1080
+ target_symbol (str): the symbol of the asset
1081
+ identifier (str): the identifier of the portfolio
1082
+ market (str): the market of the asset
1083
+
1084
+ Returns:
1085
+ List[Order]: A list of open orders that match the query parameters
1086
+ """
1087
+ query_params = {}
1088
+
1089
+ if identifier is not None:
1090
+ portfolio = self.portfolio_service.find(
1091
+ {"identifier": identifier}
1092
+ )
1093
+ query_params["portfolio"] = portfolio.id
1094
+
1095
+ if market is not None:
1096
+ portfolio = self.portfolio_service.find(
1097
+ {"market": market}
1098
+ )
1099
+ query_params["portfolio"] = portfolio.id
1100
+
1101
+ if target_symbol is not None:
1102
+ query_params["target_symbol"] = target_symbol
1103
+
1104
+ query_params["status"] = OrderStatus.OPEN.value
1105
+ return self.order_service.get_all(query_params)
1106
+
1107
+ def get_closed_orders(
1108
+ self, target_symbol=None, identifier=None, market=None, order_side=None
1109
+ ) -> List[Order]:
1110
+ """
1111
+ Function to get all closed orders. This function will return all
1112
+ closed orders that match the specified query parameters.
1113
+
1114
+ Args:
1115
+ target_symbol (str): the symbol of the asset
1116
+ identifier (str): the identifier of the portfolio
1117
+ market (str): the market of the asset
1118
+ order_side (str): the side of the order
1119
+
1120
+ Returns:
1121
+ List[Order]: A list of closed orders that
1122
+ match the query parameters
1123
+ """
1124
+ query_params = {}
1125
+
1126
+ if identifier is not None:
1127
+ portfolio = self.portfolio_service.find(
1128
+ {"identifier": identifier}
1129
+ )
1130
+ query_params["portfolio"] = portfolio.id
1131
+
1132
+ if order_side is not None:
1133
+ query_params["order_side"] = order_side
1134
+
1135
+ if market is not None:
1136
+ portfolio = self.portfolio_service.find(
1137
+ {"market": market}
1138
+ )
1139
+ query_params["portfolio"] = portfolio.id
1140
+
1141
+ if target_symbol is not None:
1142
+ query_params["target_symbol"] = target_symbol
1143
+
1144
+ query_params["status"] = OrderStatus.CLOSED.value
1145
+ return self.order_service.get_all(query_params)
1146
+
1147
+ def has_open_sell_orders(self, target_symbol, identifier=None,
1148
+ market=None):
1149
+ query_params = {}
1150
+
1151
+ if identifier is not None:
1152
+ portfolio = self.portfolio_service.find(
1153
+ {"identifier": identifier}
1154
+ )
1155
+ query_params["portfolio"] = portfolio.id
1156
+
1157
+ if market is not None:
1158
+ portfolio = self.portfolio_service.find(
1159
+ {"market": market}
1160
+ )
1161
+ query_params["portfolio"] = portfolio.id
1162
+
1163
+ query_params["target_symbol"] = target_symbol
1164
+ query_params["order_side"] = OrderSide.SELL.value
1165
+ query_params["status"] = OrderStatus.OPEN.value
1166
+ return self.order_service.exists(query_params)
1167
+
1168
+ def has_open_orders(
1169
+ self, target_symbol=None, identifier=None, market=None
1170
+ ):
1171
+ query_params = {}
1172
+
1173
+ if identifier is not None:
1174
+ portfolio = self.portfolio_service.find(
1175
+ {"identifier": identifier}
1176
+ )
1177
+ query_params["portfolio"] = portfolio.id
1178
+
1179
+ if market is not None:
1180
+ portfolio = self.portfolio_service.find(
1181
+ {"market": market}
1182
+ )
1183
+ query_params["portfolio"] = portfolio.id
1184
+
1185
+ if target_symbol is not None:
1186
+ query_params["target_symbol"] = target_symbol
1187
+
1188
+ query_params["status"] = OrderStatus.OPEN.value
1189
+ return self.order_service.exists(query_params)
1190
+
1191
+ def get_trade(
1192
+ self,
1193
+ target_symbol=None,
1194
+ trading_symbol=None,
1195
+ market=None,
1196
+ portfolio=None,
1197
+ status=None,
1198
+ order_id=None
1199
+ ) -> Trade:
1200
+ """
1201
+ Function to retrieve a trade. This function will return the first
1202
+ trade that matches the specified query parameters.
1203
+
1204
+ Args:
1205
+ market: The market of the asset
1206
+ portfolio: The portfolio of the asset
1207
+ status: The status of the trade
1208
+ order_id: The order id of the trade
1209
+ target_symbol: The symbol of the asset
1210
+ trading_symbol: The trading symbol of the asset
1211
+
1212
+ Returns:
1213
+ Trade: A instance of a trade that matches the query parameters
1214
+ """
1215
+ query_params = {}
1216
+
1217
+ if market is not None:
1218
+ query_params["market"] = market
1219
+
1220
+ if portfolio is not None:
1221
+ query_params["portfolio"] = portfolio
1222
+
1223
+ if status is not None:
1224
+ query_params["status"] = status
1225
+
1226
+ if order_id is not None:
1227
+ query_params["order_id"] = order_id
1228
+
1229
+ if target_symbol is not None:
1230
+ query_params["target_symbol"] = target_symbol
1231
+
1232
+ if trading_symbol is not None:
1233
+ query_params["trading_symbol"] = trading_symbol
1234
+
1235
+ return self.trade_service.find(query_params)
1236
+
1237
+ def get_trades(
1238
+ self,
1239
+ target_symbol=None,
1240
+ trading_symbol=None,
1241
+ market=None,
1242
+ portfolio=None,
1243
+ status=None,
1244
+ ) -> List[Trade]:
1245
+ """
1246
+ Function to get all trades. This function will return all trades
1247
+ that match the specified query parameters. If the market parameter
1248
+ is specified, the trades with the specified market will be returned.
1249
+
1250
+ Args:
1251
+ market: The market of the asset
1252
+ portfolio: The portfolio of the asset
1253
+ status: The status of the trade
1254
+ target_symbol: The symbol of the asset
1255
+ trading_symbol: The trading symbol of the asset
1256
+
1257
+ Returns:
1258
+ List[Trade]: A list of trades that match the query parameters
1259
+ """
1260
+
1261
+ query_params = {}
1262
+
1263
+ if market is not None:
1264
+ query_params["market"] = market
1265
+
1266
+ if portfolio is not None:
1267
+ query_params["portfolio"] = portfolio
1268
+
1269
+ if status is not None:
1270
+ query_params["status"] = status
1271
+
1272
+ if target_symbol is not None:
1273
+ query_params["target_symbol"] = target_symbol
1274
+
1275
+ if trading_symbol is not None:
1276
+ query_params["trading_symbol"] = trading_symbol
1277
+
1278
+ return self.trade_service.get_all({"market": market})
1279
+
1280
+ def get_closed_trades(self) -> List[Trade]:
1281
+ """
1282
+ Function to get all closed trades. This function will return all
1283
+ closed trades of the algorithm.
1284
+
1285
+ Returns:
1286
+ List[Trade]: A list of closed trades
1287
+ """
1288
+ return self.trade_service.get_all({"status": TradeStatus.CLOSED.value})
1289
+
1290
+ def count_trades(
1291
+ self,
1292
+ target_symbol=None,
1293
+ trading_symbol=None,
1294
+ market=None,
1295
+ portfolio=None
1296
+ ) -> int:
1297
+ """
1298
+ Function to count trades. This function will return the number of
1299
+ trades that match the specified query parameters.
1300
+
1301
+ Args:
1302
+ target_symbol: The symbol of the asset
1303
+ trading_symbol: The trading symbol of the asset
1304
+ market: The market of the asset
1305
+ portfolio: The portfolio of the asset
1306
+
1307
+ Returns:
1308
+ int: The number of trades that match the query parameters
1309
+ """
1310
+
1311
+ query_params = {}
1312
+
1313
+ if market is not None:
1314
+ query_params["market"] = market
1315
+
1316
+ if portfolio is not None:
1317
+ query_params["portfolio"] = portfolio
1318
+
1319
+ if target_symbol is not None:
1320
+ query_params["target_symbol"] = target_symbol
1321
+
1322
+ if trading_symbol is not None:
1323
+ query_params["trading_symbol"] = trading_symbol
1324
+
1325
+ return self.trade_service.count(query_params)
1326
+
1327
+ def get_pending_trades(
1328
+ self, target_symbol=None, market=None
1329
+ ) -> List[Trade]:
1330
+ """
1331
+ Function to get all pending trades. This function will return all
1332
+ pending trades that match the specified query parameters. If the
1333
+ target_symbol parameter is specified, the pending trades with the
1334
+ specified target symbol will be returned. If the market parameter
1335
+ is specified, the pending trades with the specified market will be
1336
+ returned.
1337
+
1338
+ Args:
1339
+ target_symbol: The symbol of the asset
1340
+ market: The market of the asset
1341
+
1342
+ Returns:
1343
+ List[Trade]: A list of pending trades that match
1344
+ the query parameters
1345
+ """
1346
+ return self.trade_service.get_all(
1347
+ {
1348
+ "status": TradeStatus.CREATED.value,
1349
+ "target_symbol": target_symbol,
1350
+ "market": market
1351
+ }
1352
+ )
1353
+
1354
+ def get_open_trades(self, target_symbol=None, market=None) -> List[Trade]:
1355
+ """
1356
+ Function to get all open trades. This function will return all
1357
+ open trades that match the specified query parameters. If the
1358
+ target_symbol parameter is specified, the open trades with the
1359
+ specified target symbol will be returned. If the market parameter
1360
+ is specified, the open trades with the specified market will be
1361
+ returned.
1362
+
1363
+ Args:
1364
+ target_symbol: The symbol of the asset
1365
+ market: The market of the asset
1366
+
1367
+ Returns:
1368
+ List[Trade]: A list of open trades that match the query parameters
1369
+ """
1370
+ return self.trade_service.get_all(
1371
+ {
1372
+ "status": TradeStatus.OPEN.value,
1373
+ "target_symbol": target_symbol,
1374
+ "market": market
1375
+ }
1376
+ )
1377
+
1378
+ def add_stop_loss(
1379
+ self,
1380
+ trade: Trade,
1381
+ percentage: float,
1382
+ trailing: bool = False,
1383
+ sell_percentage: float = 100,
1384
+ created_at: datetime = None,
1385
+ ) -> TradeStopLoss:
1386
+ """
1387
+ Function to add a stop loss to a trade.
1388
+
1389
+ Example of fixed stop loss:
1390
+ * You buy BTC at $40,000.
1391
+ * You set a SL of 5% → SL level at $38,000 (40,000 - 5%).
1392
+ * BTC price increases to $42,000 → SL level remains at $38,000.
1393
+ * BTC price drops to $38,000 → SL level reached, trade closes.
1394
+
1395
+ Example of trailing stop loss:
1396
+ * You buy BTC at $40,000.
1397
+ * You set a TSL of 5%, setting the sell price at $38,000.
1398
+ * BTC price increases to $42,000 → New TSL level
1399
+ at $39,900 (42,000 - 5%).
1400
+ * BTC price drops to $39,900 → SL level reached, trade closes.
1401
+
1402
+ Args:
1403
+ trade (Trade): Trade object representing the trade
1404
+ percentage (float): float representing the percentage
1405
+ of the open price that the stop loss should
1406
+ be set at. This must be a positive
1407
+ number, e.g. 5 for 5%, or 10 for 10%.
1408
+ trailing (bool): Whether the stop loss should be trailing
1409
+ or fixed.
1410
+ sell_percentage (float): float representing the
1411
+ percentage of the trade that should be sold if the
1412
+ stop loss is triggered
1413
+ created_at: datetime: The date and time when the stop loss
1414
+ was created. If not specified, the current date and time
1415
+ will be used.
1416
+
1417
+ Returns:
1418
+ None
1419
+ """
1420
+ return self.trade_service.add_stop_loss(
1421
+ trade,
1422
+ percentage=percentage,
1423
+ trailing=trailing,
1424
+ sell_percentage=sell_percentage,
1425
+ created_at=created_at,
1426
+ )
1427
+
1428
+ def add_take_profit(
1429
+ self,
1430
+ trade: Trade,
1431
+ percentage: float,
1432
+ trailing: bool = False,
1433
+ sell_percentage: float = 100,
1434
+ created_at: datetime = None,
1435
+ ) -> TradeTakeProfit:
1436
+ """
1437
+ Function to add a take profit to a trade. This function will add a
1438
+ take profit to the specified trade. If the take profit is triggered,
1439
+ the trade will be closed.
1440
+
1441
+ Example of take profit:
1442
+ * You buy BTC at $40,000.
1443
+ * You set a TP of 5% → TP level at $42,000 (40,000 + 5%).
1444
+ * BTC rises to $42,000 → TP level reached, trade
1445
+ closes, securing profit.
1446
+
1447
+ Example of trailing take profit:
1448
+ * You buy BTC at $40,000
1449
+ * You set a TTP of 5%, setting the sell price at $42,000.
1450
+ * BTC rises to $42,000 → TTP level stays at $42,000.
1451
+ * BTC rises to $45,000 → New TTP level at $42,750.
1452
+ * BTC drops to $42,750 → Trade closes, securing profit.
1453
+
1454
+ Args:
1455
+ trade (Trade): Trade object representing the trade
1456
+ percentage (float): float representing the percentage
1457
+ of the open price that the stop loss should
1458
+ be set at. This must be a positive
1459
+ number, e.g. 5 for 5%, or 10 for 10%.
1460
+ trailing (bool): Whether the take profit should be trailing
1461
+ or fixed.
1462
+ sell_percentage (float): float representing the
1463
+ percentage of the trade that should be sold if the
1464
+ stop loss is triggered
1465
+ created_at: datetime: The date and time when the take profit
1466
+ was created. If not specified, the current date and time
1467
+ will be used.
1468
+
1469
+ Returns:
1470
+ None
1471
+ """
1472
+ return self.trade_service.add_take_profit(
1473
+ trade,
1474
+ percentage=percentage,
1475
+ trailing=trailing,
1476
+ sell_percentage=sell_percentage,
1477
+ created_at=created_at,
1478
+ )
1479
+
1480
+ def close_trade(self, trade, precision=None) -> None:
1481
+ """
1482
+ Function to close a trade. This function will close a trade by
1483
+ creating a market order to sell the position. If the precision
1484
+ parameter is specified, the amount of the order will be rounded
1485
+ down to the specified precision.
1486
+
1487
+ Args:
1488
+ trade: Trade - The trade to close
1489
+ precision: int - The precision of the amount
1490
+
1491
+ Returns:
1492
+ None
1493
+ """
1494
+ trade = self.trade_service.get(trade.id)
1495
+
1496
+ if TradeStatus.CLOSED.equals(trade.status):
1497
+ raise OperationalException("Trade already closed.")
1498
+
1499
+ if trade.available_amount <= 0:
1500
+ raise OperationalException("Trade has no amount to close.")
1501
+
1502
+ position_id = trade.orders[0].position_id
1503
+ portfolio = self.portfolio_service.find({"position": position_id})
1504
+ position = self.position_service.find(
1505
+ {"portfolio": portfolio.id, "symbol": trade.target_symbol}
1506
+ )
1507
+ amount = trade.available_amount
1508
+
1509
+ if precision is not None:
1510
+ amount = RoundingService.round_down(amount, precision)
1511
+
1512
+ if position.get_amount() < amount:
1513
+ logger.warning(
1514
+ f"Order amount {amount} is larger then amount "
1515
+ f"of available {position.symbol} "
1516
+ f"position: {position.get_amount()}, "
1517
+ f"changing order amount to size of position"
1518
+ )
1519
+ amount = position.get_amount()
1520
+
1521
+ ticker = self.data_provider_service.get_ticker_data(
1522
+ symbol=trade.symbol,
1523
+ market=portfolio.market,
1524
+ date=self.config[INDEX_DATETIME]
1525
+ )
1526
+ logger.info(f"Closing trade {trade.id} {trade.symbol}")
1527
+ self.order_service.create(
1528
+ {
1529
+ "portfolio_id": portfolio.id,
1530
+ "trading_symbol": trade.trading_symbol,
1531
+ "target_symbol": trade.target_symbol,
1532
+ "amount": amount,
1533
+ "order_side": OrderSide.SELL.value,
1534
+ "order_type": OrderType.LIMIT.value,
1535
+ "price": ticker["bid"],
1536
+ }
1537
+ )
1538
+
1539
+ def get_number_of_positions(self):
1540
+ """
1541
+ Returns the number of positions that have a positive amount.
1542
+
1543
+ Returns:
1544
+ int: The number of positions
1545
+ """
1546
+ return self.position_service.count({"amount_gt": 0})
1547
+
1548
+ def has_trading_symbol_position_available(
1549
+ self,
1550
+ amount_gt=None,
1551
+ amount_gte=None,
1552
+ percentage_of_portfolio=None,
1553
+ market=None
1554
+ ):
1555
+ """
1556
+ Checks if there is a position available for the trading symbol of the
1557
+ portfolio. If the amount_gt or amount_gte parameters are specified,
1558
+ the amount of the position must be greater than the specified amount.
1559
+ If the percentage_of_portfolio parameter is specified, the amount of
1560
+ the position must be greater than the net_size of the
1561
+ portfolio.
1562
+
1563
+ Parameters:
1564
+ amount_gt: The amount of the position must be greater than this
1565
+ amount.
1566
+ :param amount_gte: The amount of the position must be greater than
1567
+ or equal to this amount.
1568
+ :param percentage_of_portfolio: The amount of the position must be
1569
+ greater than the net_size of the portfolio.
1570
+ :param market: The market of the portfolio.
1571
+ :return: True if there is a trading symbol position available with the
1572
+ specified parameters, False otherwise.
1573
+ """
1574
+ portfolio = self.portfolio_service.find({"market": market})
1575
+ position = self.position_service.find(
1576
+ {"portfolio": portfolio.id, "symbol": portfolio.trading_symbol}
1577
+ )
1578
+
1579
+ if amount_gt is not None:
1580
+ return position.get_amount() > amount_gt
1581
+
1582
+ if amount_gte is not None:
1583
+ return position.get_amount() >= amount_gte
1584
+
1585
+ if percentage_of_portfolio is not None:
1586
+ net_size = portfolio.get_net_size()
1587
+ return position.get_amount() >= net_size \
1588
+ * percentage_of_portfolio / 100
1589
+
1590
+ return position.get_amount() > 0
1591
+
1592
+ def get_pending_orders(
1593
+ self, order_side=None, target_symbol=None, portfolio_id=None
1594
+ ):
1595
+ """
1596
+ Function to get all pending orders of the algorithm. If the
1597
+ portfolio_id parameter is specified, the function will return
1598
+ all pending orders of the portfolio with the specified id.
1599
+ """
1600
+ query_params = {}
1601
+
1602
+ if portfolio_id:
1603
+ query_params["portfolio"] = portfolio_id
1604
+
1605
+ if target_symbol:
1606
+ query_params["target_symbol"] = target_symbol
1607
+
1608
+ if order_side:
1609
+ query_params["order_side"] = order_side
1610
+
1611
+ return self.order_service.get_all({"status": OrderStatus.OPEN.value})
1612
+
1613
+ def get_unfilled_buy_value(self):
1614
+ """
1615
+ Returns the total value of all unfilled buy orders.
1616
+ """
1617
+ pending_orders = self.get_pending_orders(
1618
+ order_side=OrderSide.BUY.value
1619
+ )
1620
+
1621
+ return sum(
1622
+ [order.get_remaining() * order.get_price()
1623
+ for order in pending_orders]
1624
+ )
1625
+
1626
+ def get_unfilled_sell_value(self):
1627
+ """
1628
+ Returns the total value of all unfilled buy orders.
1629
+ """
1630
+ pending_orders = self.get_pending_orders(
1631
+ order_side=OrderSide.SELL.value
1632
+ )
1633
+
1634
+ return sum(
1635
+ [order.get_remaining() * order.get_price()
1636
+ for order in pending_orders]
1637
+ )
1638
+
1639
+ def get_market_credential(self, market) -> MarketCredential:
1640
+ """
1641
+ Function to get the market credential for a given market.
1642
+
1643
+ Args:
1644
+ market: The market to get the credential for
1645
+
1646
+ Returns:
1647
+ MarketCredential: The market credential for the given market
1648
+ """
1649
+ return self.market_credential_service.get(market)
1650
+
1651
+ def get_market_credentials(self) -> List[MarketCredential]:
1652
+ """
1653
+ Function to get all market credentials.
1654
+
1655
+ Returns:
1656
+ List[MarketCredential]: A list of all market credentials
1657
+ """
1658
+ return self.market_credential_service.get_all()
1659
+
1660
+ def get_trading_symbol(self, portfolio_id=None):
1661
+ """
1662
+ Function to get the trading symbol of a portfolio. If the
1663
+ portfolio_id parameter is specified, the function will return
1664
+ the trading symbol of the portfolio with the specified id.
1665
+
1666
+ Args:
1667
+ portfolio_id: The id of the portfolio to get the trading symbol for
1668
+
1669
+ Returns:
1670
+ str: The trading symbol of the portfolio
1671
+ """
1672
+ if portfolio_id is None:
1673
+ if self.portfolio_service.count() > 1:
1674
+ raise OperationalException(
1675
+ "Multiple portfolios found. Please specify a "
1676
+ "portfolio identifier."
1677
+ )
1678
+ portfolio = self.portfolio_service.get_all()[0]
1679
+ else:
1680
+ portfolio = self.portfolio_service.get(portfolio_id)
1681
+
1682
+ return portfolio.trading_symbol
1683
+
1684
+ def get_take_profits(
1685
+ self, triggered: bool = None
1686
+ ) -> List[TradeTakeProfit]:
1687
+ """
1688
+ Function to get all take profits. If the triggered parameter
1689
+ is specified, the function will return all take profits that
1690
+ match the triggered status.
1691
+
1692
+ Args:
1693
+ triggered (bool): The triggered status of the take profits
1694
+
1695
+ Returns:
1696
+ List[TradeTakeProfit]: A list of take profits
1697
+ """
1698
+ query_params = {}
1699
+
1700
+ if triggered is not None:
1701
+ query_params["triggered"] = triggered
1702
+
1703
+ return self.trade_take_profit_service.get_all(query_params)
1704
+
1705
+ def get_stop_losses(
1706
+ self, triggered: bool = None
1707
+ ) -> List[TradeStopLoss]:
1708
+ """
1709
+ Function to get all stop losses. If the triggered parameter
1710
+ is specified, the function will return all stop losses that
1711
+ match the triggered status.
1712
+
1713
+ Args:
1714
+ triggered (bool): The triggered status of the stop losses
1715
+
1716
+ Returns:
1717
+ List[TradeStopLoss]: A list of stop losses
1718
+ """
1719
+ query_params = {}
1720
+
1721
+ if triggered is not None:
1722
+ query_params["triggered"] = triggered
1723
+
1724
+ return self.trade_stop_loss_service.get_all(query_params)