investing-algorithm-framework 3.7.0__py3-none-any.whl → 7.19.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of investing-algorithm-framework might be problematic. Click here for more details.
- investing_algorithm_framework/__init__.py +168 -45
- investing_algorithm_framework/app/__init__.py +32 -1
- investing_algorithm_framework/app/algorithm/__init__.py +7 -0
- investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +1933 -589
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1725 -0
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +27 -0
- investing_algorithm_framework/app/reporting/ascii.py +921 -0
- investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
- investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
- investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
- investing_algorithm_framework/app/reporting/generate.py +185 -0
- investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
- investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
- investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
- investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
- investing_algorithm_framework/app/stateless/action_handlers/__init__.py +4 -2
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +1 -1
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
- investing_algorithm_framework/app/strategy.py +664 -84
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/app/web/__init__.py +2 -1
- investing_algorithm_framework/app/web/create_app.py +4 -2
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +226 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
- investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
- investing_algorithm_framework/cli/initialize_app.py +603 -0
- investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
- investing_algorithm_framework/cli/templates/app.py.template +18 -0
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
- investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
- investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
- investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
- investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
- investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
- investing_algorithm_framework/cli/templates/env.example.template +2 -0
- investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
- investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
- investing_algorithm_framework/cli/templates/readme.md.template +135 -0
- investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
- investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
- investing_algorithm_framework/create_app.py +40 -6
- investing_algorithm_framework/dependency_container.py +72 -56
- investing_algorithm_framework/domain/__init__.py +71 -47
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +59 -91
- investing_algorithm_framework/domain/constants.py +13 -38
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +3 -2
- investing_algorithm_framework/domain/exceptions.py +51 -1
- investing_algorithm_framework/domain/models/__init__.py +17 -12
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/data/data_type.py +46 -0
- investing_algorithm_framework/domain/models/event.py +35 -0
- investing_algorithm_framework/domain/models/market/market_credential.py +55 -1
- investing_algorithm_framework/domain/models/order/order.py +77 -83
- investing_algorithm_framework/domain/models/order/order_status.py +2 -2
- investing_algorithm_framework/domain/models/order/order_type.py +1 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +81 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +26 -3
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +12 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
- investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
- investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
- investing_algorithm_framework/domain/models/time_frame.py +37 -0
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +66 -2
- investing_algorithm_framework/domain/models/trade/__init__.py +8 -1
- investing_algorithm_framework/domain/models/trade/trade.py +295 -171
- investing_algorithm_framework/domain/models/trade/trade_status.py +9 -2
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -0
- investing_algorithm_framework/domain/services/__init__.py +2 -9
- investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +0 -6
- investing_algorithm_framework/domain/services/state_handler.py +38 -0
- investing_algorithm_framework/domain/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +12 -7
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/dates.py +57 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +53 -0
- investing_algorithm_framework/domain/utils/random.py +29 -0
- investing_algorithm_framework/download_data.py +108 -0
- investing_algorithm_framework/infrastructure/__init__.py +31 -18
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
- investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
- investing_algorithm_framework/infrastructure/models/__init__.py +6 -11
- investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -1
- investing_algorithm_framework/infrastructure/models/order/order.py +35 -49
- investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
- investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
- investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +1 -1
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -0
- investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -5
- investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
- investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
- investing_algorithm_framework/infrastructure/repositories/__init__.py +8 -0
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +5 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +1 -1
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +81 -27
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +4 -4
- investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
- investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
- investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
- investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
- investing_algorithm_framework/services/__init__.py +113 -16
- investing_algorithm_framework/services/backtesting/__init__.py +0 -7
- investing_algorithm_framework/services/backtesting/backtest_service.py +566 -359
- investing_algorithm_framework/services/configuration_service.py +77 -11
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/services/market_credential_service.py +16 -1
- investing_algorithm_framework/services/metrics/__init__.py +114 -0
- investing_algorithm_framework/services/metrics/alpha.py +0 -0
- investing_algorithm_framework/services/metrics/beta.py +0 -0
- investing_algorithm_framework/services/metrics/cagr.py +60 -0
- investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
- investing_algorithm_framework/services/metrics/drawdown.py +181 -0
- investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
- investing_algorithm_framework/services/metrics/exposure.py +210 -0
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
- investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
- investing_algorithm_framework/services/metrics/recovery.py +113 -0
- investing_algorithm_framework/services/metrics/returns.py +452 -0
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
- investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
- investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
- investing_algorithm_framework/services/metrics/ulcer.py +0 -0
- investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/services/metrics/win_rate.py +177 -0
- investing_algorithm_framework/services/order_service/__init__.py +3 -1
- investing_algorithm_framework/services/order_service/order_backtest_service.py +76 -89
- investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
- investing_algorithm_framework/services/order_service/order_service.py +407 -326
- investing_algorithm_framework/services/portfolios/__init__.py +3 -1
- investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +37 -3
- investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +22 -8
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
- investing_algorithm_framework/services/portfolios/portfolio_service.py +96 -28
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +97 -28
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +116 -313
- investing_algorithm_framework/services/positions/__init__.py +7 -0
- investing_algorithm_framework/services/positions/position_service.py +210 -0
- investing_algorithm_framework/services/repository_service.py +8 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
- investing_algorithm_framework/services/trade_service/__init__.py +7 -1
- investing_algorithm_framework/services/trade_service/trade_service.py +1013 -315
- investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
- investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
- investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
- investing_algorithm_framework-7.19.15.dist-info/RECORD +263 -0
- investing_algorithm_framework-7.19.15.dist-info/entry_points.txt +3 -0
- investing_algorithm_framework/app/algorithm.py +0 -1105
- investing_algorithm_framework/domain/graphs.py +0 -382
- investing_algorithm_framework/domain/metrics/__init__.py +0 -6
- investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -11
- investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -43
- investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
- investing_algorithm_framework/domain/models/backtesting/backtest_report.py +0 -580
- investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -243
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/services/market_data_sources.py +0 -344
- investing_algorithm_framework/domain/services/market_service.py +0 -153
- investing_algorithm_framework/domain/singleton.py +0 -9
- investing_algorithm_framework/domain/utils/backtesting.py +0 -472
- investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -12
- investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -559
- investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -254
- investing_algorithm_framework/infrastructure/models/market_data_sources/us_treasury_yield.py +0 -47
- investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
- investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -455
- investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
- investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
- investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -350
- investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py +0 -53
- investing_algorithm_framework/services/backtesting/graphs.py +0 -61
- investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -8
- investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -150
- investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -189
- investing_algorithm_framework/services/position_service.py +0 -31
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -264
- investing_algorithm_framework-3.7.0.dist-info/METADATA +0 -339
- investing_algorithm_framework-3.7.0.dist-info/RECORD +0 -147
- /investing_algorithm_framework/{domain → services}/metrics/price_efficiency.py +0 -0
- /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
- {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
- {investing_algorithm_framework-3.7.0.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
|
@@ -1,401 +1,1099 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from datetime import datetime, timezone
|
|
2
3
|
from queue import PriorityQueue
|
|
3
|
-
from typing import
|
|
4
|
+
from typing import Union
|
|
4
5
|
|
|
5
|
-
from investing_algorithm_framework.domain import OrderStatus,
|
|
6
|
-
Trade,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
PositionService
|
|
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
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class TradeService:
|
|
16
|
+
class TradeService(RepositoryService):
|
|
17
17
|
"""
|
|
18
|
-
|
|
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.
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
def __init__(
|
|
22
25
|
self,
|
|
26
|
+
trade_repository,
|
|
27
|
+
order_repository,
|
|
28
|
+
trade_stop_loss_repository,
|
|
29
|
+
trade_take_profit_repository,
|
|
30
|
+
position_repository,
|
|
23
31
|
portfolio_repository,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
market_data_source_service
|
|
32
|
+
configuration_service,
|
|
33
|
+
order_metadata_repository
|
|
27
34
|
):
|
|
35
|
+
super(TradeService, self).__init__(trade_repository)
|
|
36
|
+
self.order_repository = order_repository
|
|
28
37
|
self.portfolio_repository = portfolio_repository
|
|
29
|
-
self.
|
|
30
|
-
self.
|
|
31
|
-
|
|
32
|
-
self.
|
|
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
|
|
33
43
|
|
|
34
|
-
def
|
|
44
|
+
def create_trade_from_buy_order(self, buy_order) -> Union[Trade, None]:
|
|
35
45
|
"""
|
|
36
|
-
|
|
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.
|
|
37
51
|
|
|
38
|
-
:
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
41
57
|
"""
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
if buy_order.status in \
|
|
60
|
+
[
|
|
61
|
+
OrderStatus.CANCELED.value,
|
|
62
|
+
OrderStatus.EXPIRED.value,
|
|
63
|
+
OrderStatus.REJECTED.value
|
|
64
|
+
]:
|
|
65
|
+
return None
|
|
45
66
|
|
|
46
|
-
|
|
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
|
+
}
|
|
47
78
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
})
|
|
61
155
|
else:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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"]
|
|
66
197
|
})
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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"]
|
|
71
214
|
})
|
|
72
215
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
78
224
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
225
|
+
Returns:
|
|
226
|
+
Trade object
|
|
227
|
+
"""
|
|
82
228
|
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
85
243
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
- first_buy_order.get_trade_closed_amount()
|
|
244
|
+
# Check if config environment has value BACKTEST
|
|
245
|
+
config = self.configuration_service.get_config()
|
|
246
|
+
environment = config[ENVIRONMENT]
|
|
90
247
|
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
first_buy_order.set_filled(remaining)
|
|
248
|
+
if Environment.BACKTEST.equals(environment):
|
|
249
|
+
last_reported_price_date = \
|
|
250
|
+
config[INDEX_DATETIME]
|
|
95
251
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
for
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
symbol=symbol, market=market
|
|
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
|
|
105
260
|
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if closed_amount is not None:
|
|
114
|
-
amount = amount - closed_amount
|
|
115
|
-
|
|
116
|
-
trades.append(
|
|
117
|
-
Trade(
|
|
118
|
-
buy_order_id=buy_order.id,
|
|
119
|
-
target_symbol=buy_order.get_target_symbol(),
|
|
120
|
-
trading_symbol=buy_order.get_trading_symbol(),
|
|
121
|
-
amount=amount,
|
|
122
|
-
open_price=buy_order.get_price(),
|
|
123
|
-
opened_at=buy_order.get_created_at(),
|
|
124
|
-
current_price=current_price
|
|
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
|
|
125
268
|
)
|
|
126
|
-
|
|
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)
|
|
127
276
|
|
|
128
|
-
return
|
|
277
|
+
return super(TradeService, self).update(trade_id, data)
|
|
129
278
|
|
|
130
|
-
def
|
|
279
|
+
def _create_trade_metadata_with_sell_order_and_trades(
|
|
280
|
+
self, sell_order, trades
|
|
281
|
+
):
|
|
131
282
|
"""
|
|
132
|
-
|
|
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
|
+
|
|
133
294
|
|
|
134
|
-
:param market: str representing the market
|
|
135
|
-
:param portfolio_id: str representing the portfolio id
|
|
136
|
-
:return: list of Trade objects
|
|
137
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
|
|
138
300
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
141
309
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
})
|
|
148
317
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
172
409
|
)
|
|
173
410
|
|
|
174
|
-
|
|
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
|
+
})
|
|
175
430
|
|
|
176
|
-
|
|
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:
|
|
177
453
|
"""
|
|
178
|
-
|
|
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
|
|
179
465
|
|
|
180
|
-
:
|
|
181
|
-
|
|
466
|
+
Returns:
|
|
467
|
+
Trade: Trade object representing the updated trade object
|
|
182
468
|
"""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
187
476
|
})
|
|
188
|
-
return [
|
|
189
|
-
Trade(
|
|
190
|
-
buy_order_id=order.id,
|
|
191
|
-
target_symbol=order.get_target_symbol(),
|
|
192
|
-
trading_symbol=order.get_trading_symbol(),
|
|
193
|
-
amount=order.get_amount(),
|
|
194
|
-
open_price=order.get_price(),
|
|
195
|
-
closed_price=order.get_trade_closed_price(),
|
|
196
|
-
closed_at=order.get_trade_closed_at(),
|
|
197
|
-
opened_at=order.get_created_at()
|
|
198
|
-
) for order in buy_orders
|
|
199
|
-
if order.get_trade_closed_at() is not None
|
|
200
|
-
]
|
|
201
477
|
|
|
202
|
-
|
|
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:
|
|
203
540
|
"""
|
|
204
|
-
|
|
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.
|
|
205
547
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
210
553
|
|
|
211
|
-
|
|
554
|
+
Returns:
|
|
555
|
+
Trade object
|
|
212
556
|
"""
|
|
557
|
+
trade = self.find({"buy_order": buy_order.id})
|
|
558
|
+
filled = buy_order.get_filled()
|
|
559
|
+
amount = buy_order.get_amount()
|
|
213
560
|
|
|
214
|
-
if
|
|
215
|
-
|
|
561
|
+
if filled is None:
|
|
562
|
+
filled = trade.filled_amount + filled_difference
|
|
216
563
|
|
|
217
|
-
|
|
564
|
+
remaining = buy_order.get_remaining()
|
|
218
565
|
|
|
219
|
-
if
|
|
566
|
+
if remaining is None:
|
|
567
|
+
remaining = trade.remaining - filled_difference
|
|
568
|
+
|
|
569
|
+
if trade is None:
|
|
220
570
|
raise OperationalException(
|
|
221
|
-
"
|
|
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
|
|
222
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
|
|
223
712
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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]
|
|
240
730
|
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
"portfolio_id": portfolio.id,
|
|
250
|
-
"trading_symbol": order.get_trading_symbol(),
|
|
251
|
-
"target_symbol": order.get_target_symbol(),
|
|
252
|
-
"amount": amount,
|
|
253
|
-
"order_side": OrderSide.SELL.value,
|
|
254
|
-
"order_type": OrderType.LIMIT.value,
|
|
255
|
-
"price": ticker["bid"],
|
|
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]
|
|
256
739
|
}
|
|
257
|
-
|
|
740
|
+
self.update(open_trade.id, update_data)
|
|
258
741
|
|
|
259
|
-
def
|
|
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:
|
|
260
750
|
"""
|
|
261
|
-
|
|
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.
|
|
262
758
|
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
265
779
|
"""
|
|
780
|
+
trade = self.get(trade.id)
|
|
266
781
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
269
787
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
|
|
275
|
-
})
|
|
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
|
+
)
|
|
276
793
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
)
|
|
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)
|
|
290
806
|
|
|
291
|
-
|
|
292
|
-
|
|
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.
|
|
293
819
|
|
|
294
|
-
|
|
295
|
-
|
|
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.
|
|
296
825
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
trades = [
|
|
304
|
-
trade for trade in trades
|
|
305
|
-
if trade.closed_at is not None
|
|
306
|
-
]
|
|
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.
|
|
307
832
|
|
|
308
|
-
|
|
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.
|
|
309
845
|
|
|
310
|
-
|
|
846
|
+
Returns:
|
|
847
|
+
None
|
|
311
848
|
"""
|
|
312
|
-
|
|
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)
|
|
313
874
|
|
|
314
|
-
|
|
315
|
-
:param amount_to_close: float representing the amount to close
|
|
316
|
-
:return: None
|
|
875
|
+
def get_triggered_stop_loss_orders(self):
|
|
317
876
|
"""
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
f"amount to close: {amount_to_close}"
|
|
321
|
-
)
|
|
877
|
+
Function to get all triggered stop loss orders. This function will
|
|
878
|
+
return a list of trade ids that have triggered stop losses.
|
|
322
879
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
)
|
|
331
|
-
matching_buy_orders = [
|
|
332
|
-
buy_order for buy_order in matching_buy_orders
|
|
333
|
-
if buy_order.get_trade_closed_at() is None
|
|
334
|
-
]
|
|
335
|
-
order_queue = PriorityQueue()
|
|
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 = []
|
|
336
887
|
|
|
337
|
-
|
|
338
|
-
|
|
888
|
+
# Group trades by target symbol
|
|
889
|
+
stop_losses_by_target_symbol = {}
|
|
339
890
|
|
|
340
|
-
|
|
341
|
-
|
|
891
|
+
for open_trade in open_trades:
|
|
892
|
+
triggered_stop_losses = []
|
|
342
893
|
|
|
343
|
-
|
|
344
|
-
buy_order = order_queue.get()
|
|
345
|
-
closed_amount = buy_order.get_trade_closed_amount()
|
|
894
|
+
for stop_loss in open_trade.stop_losses:
|
|
346
895
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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)
|
|
350
901
|
|
|
351
|
-
|
|
902
|
+
to_be_saved_stop_loss_objects.append(stop_loss)
|
|
352
903
|
|
|
353
|
-
if
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
368
948
|
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
386
1054
|
)
|
|
387
|
-
amount_to_close = 0
|
|
388
1055
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|