bbstrader 0.1.8__py3-none-any.whl → 0.1.91__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 bbstrader might be problematic. Click here for more details.
- bbstrader/__ini__.py +4 -2
- bbstrader/btengine/__init__.py +5 -5
- bbstrader/btengine/backtest.py +51 -10
- bbstrader/btengine/data.py +147 -55
- bbstrader/btengine/event.py +4 -1
- bbstrader/btengine/execution.py +125 -23
- bbstrader/btengine/performance.py +4 -7
- bbstrader/btengine/portfolio.py +34 -13
- bbstrader/btengine/strategy.py +466 -6
- bbstrader/config.py +111 -0
- bbstrader/metatrader/__init__.py +4 -4
- bbstrader/metatrader/account.py +357 -55
- bbstrader/metatrader/rates.py +234 -31
- bbstrader/metatrader/risk.py +35 -24
- bbstrader/metatrader/trade.py +361 -173
- bbstrader/metatrader/utils.py +2 -53
- bbstrader/models/factors.py +0 -0
- bbstrader/models/ml.py +0 -0
- bbstrader/models/optimization.py +0 -0
- bbstrader/trading/__init__.py +1 -1
- bbstrader/trading/execution.py +329 -215
- bbstrader/trading/scripts.py +57 -0
- bbstrader/trading/strategies.py +49 -71
- bbstrader/tseries.py +274 -39
- {bbstrader-0.1.8.dist-info → bbstrader-0.1.91.dist-info}/METADATA +11 -3
- bbstrader-0.1.91.dist-info/RECORD +31 -0
- {bbstrader-0.1.8.dist-info → bbstrader-0.1.91.dist-info}/WHEEL +1 -1
- bbstrader-0.1.8.dist-info/RECORD +0 -26
- {bbstrader-0.1.8.dist-info → bbstrader-0.1.91.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.8.dist-info → bbstrader-0.1.91.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -2,17 +2,41 @@ import os
|
|
|
2
2
|
import csv
|
|
3
3
|
import time
|
|
4
4
|
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
import MetaTrader5 as Mt5
|
|
7
8
|
from logging import Logger
|
|
8
9
|
from tabulate import tabulate
|
|
9
|
-
from typing import
|
|
10
|
+
from typing import (
|
|
11
|
+
List,
|
|
12
|
+
Tuple,
|
|
13
|
+
Dict,
|
|
14
|
+
Any,
|
|
15
|
+
Optional,
|
|
16
|
+
Literal,
|
|
17
|
+
Callable
|
|
18
|
+
)
|
|
10
19
|
from bbstrader.btengine.performance import create_sharpe_ratio
|
|
11
20
|
from bbstrader.metatrader.risk import RiskManagement
|
|
12
|
-
from bbstrader.metatrader.account import
|
|
21
|
+
from bbstrader.metatrader.account import(
|
|
22
|
+
check_mt5_connection,
|
|
23
|
+
INIT_MSG
|
|
24
|
+
)
|
|
13
25
|
from bbstrader.metatrader.utils import (
|
|
14
|
-
TimeFrame,
|
|
15
|
-
|
|
26
|
+
TimeFrame,
|
|
27
|
+
TradePosition,
|
|
28
|
+
TickInfo,
|
|
29
|
+
raise_mt5_error,
|
|
30
|
+
trade_retcode_message
|
|
31
|
+
)
|
|
32
|
+
from bbstrader.config import config_logger, BBSTRADER_DIR
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
'Trade',
|
|
37
|
+
'create_trade_instance',
|
|
38
|
+
]
|
|
39
|
+
|
|
16
40
|
|
|
17
41
|
class Trade(RiskManagement):
|
|
18
42
|
"""
|
|
@@ -75,7 +99,6 @@ class Trade(RiskManagement):
|
|
|
75
99
|
expert_id: int = 9818,
|
|
76
100
|
version: str = '1.0',
|
|
77
101
|
target: float = 5.0,
|
|
78
|
-
be_on_trade_open: bool = True,
|
|
79
102
|
start_time: str = "1:00",
|
|
80
103
|
finishing_time: str = "23:00",
|
|
81
104
|
ending_time: str = "23:30",
|
|
@@ -93,8 +116,7 @@ class Trade(RiskManagement):
|
|
|
93
116
|
expert_id (int): The `unique ID` used to identify the expert advisor
|
|
94
117
|
or the strategy used on the symbol.
|
|
95
118
|
version (str): The `version` of the expert advisor.
|
|
96
|
-
target (float): `Trading period (day, week, month) profit target` in percentage
|
|
97
|
-
be_on_trade_open (bool): Whether to check for break-even when opening a trade.
|
|
119
|
+
target (float): `Trading period (day, week, month) profit target` in percentage.
|
|
98
120
|
start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
|
|
99
121
|
finishing_time (str): The time after which no new position can be opened.
|
|
100
122
|
ending_time (str): The time after which any open position will be closed.
|
|
@@ -131,7 +153,6 @@ class Trade(RiskManagement):
|
|
|
131
153
|
self.expert_id = expert_id
|
|
132
154
|
self.version = version
|
|
133
155
|
self.target = target
|
|
134
|
-
self.be_on_trade_open = be_on_trade_open
|
|
135
156
|
self.verbose = verbose
|
|
136
157
|
self.start = start_time
|
|
137
158
|
self.end = ending_time
|
|
@@ -145,13 +166,13 @@ class Trade(RiskManagement):
|
|
|
145
166
|
":")
|
|
146
167
|
self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
|
|
147
168
|
|
|
148
|
-
self.buy_positions
|
|
149
|
-
self.sell_positions
|
|
150
|
-
self.opened_positions
|
|
151
|
-
self.opened_orders
|
|
152
|
-
self.break_even_status
|
|
153
|
-
self.break_even_points
|
|
154
|
-
self.trail_after_points =
|
|
169
|
+
self.buy_positions = []
|
|
170
|
+
self.sell_positions = []
|
|
171
|
+
self.opened_positions = []
|
|
172
|
+
self.opened_orders = []
|
|
173
|
+
self.break_even_status = []
|
|
174
|
+
self.break_even_points = {}
|
|
175
|
+
self.trail_after_points = []
|
|
155
176
|
|
|
156
177
|
self.initialize()
|
|
157
178
|
self.select_symbol()
|
|
@@ -168,7 +189,9 @@ class Trade(RiskManagement):
|
|
|
168
189
|
def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
|
|
169
190
|
"""Get the logger object"""
|
|
170
191
|
if isinstance(logger, str):
|
|
171
|
-
|
|
192
|
+
log_path = BBSTRADER_DIR / 'logs'
|
|
193
|
+
log_path.mkdir(exist_ok=True)
|
|
194
|
+
return config_logger(f'{log_path}/{logger}', consol_log)
|
|
172
195
|
return logger
|
|
173
196
|
|
|
174
197
|
def initialize(self):
|
|
@@ -184,8 +207,7 @@ class Trade(RiskManagement):
|
|
|
184
207
|
try:
|
|
185
208
|
if self.verbose:
|
|
186
209
|
print("\nInitializing the basics.")
|
|
187
|
-
|
|
188
|
-
raise_mt5_error(message=INIT_MSG)
|
|
210
|
+
check_mt5_connection()
|
|
189
211
|
if self.verbose:
|
|
190
212
|
print(
|
|
191
213
|
f"You are running the @{self.expert_name} Expert advisor,"
|
|
@@ -306,14 +328,10 @@ class Trade(RiskManagement):
|
|
|
306
328
|
"""
|
|
307
329
|
stats, additional_stats = self.get_stats()
|
|
308
330
|
|
|
309
|
-
deals = stats["deals"]
|
|
310
|
-
wins = stats["win_trades"]
|
|
311
|
-
losses = stats["loss_trades"]
|
|
312
331
|
profit = round(stats["profit"], 2)
|
|
313
332
|
win_rate = stats["win_rate"]
|
|
314
333
|
total_fees = round(stats["total_fees"], 3)
|
|
315
334
|
average_fee = round(stats["average_fee"], 3)
|
|
316
|
-
profitability = additional_stats["profitability"]
|
|
317
335
|
currency = self.get_account_info().currency
|
|
318
336
|
net_profit = round((profit + total_fees), 2)
|
|
319
337
|
trade_risk = round(self.get_currency_risk() * -1, 2)
|
|
@@ -321,9 +339,9 @@ class Trade(RiskManagement):
|
|
|
321
339
|
|
|
322
340
|
# Formatting the statistics output
|
|
323
341
|
session_data = [
|
|
324
|
-
["Total Trades", deals],
|
|
325
|
-
["Winning Trades",
|
|
326
|
-
["Losing Trades",
|
|
342
|
+
["Total Trades", stats["deals"]],
|
|
343
|
+
["Winning Trades", stats["win_trades"]],
|
|
344
|
+
["Losing Trades", stats["loss_trades"]],
|
|
327
345
|
["Session Profit", f"{profit} {currency}"],
|
|
328
346
|
["Total Fees", f"{total_fees} {currency}"],
|
|
329
347
|
["Average Fees", f"{average_fee} {currency}"],
|
|
@@ -333,9 +351,10 @@ class Trade(RiskManagement):
|
|
|
333
351
|
["Risk Reward Ratio", self.rr],
|
|
334
352
|
["Win Rate", f"{win_rate}%"],
|
|
335
353
|
["Sharpe Ratio", self.sharpe()],
|
|
336
|
-
["Trade Profitability", profitability],
|
|
354
|
+
["Trade Profitability", additional_stats["profitability"]],
|
|
337
355
|
]
|
|
338
|
-
session_table = tabulate(
|
|
356
|
+
session_table = tabulate(
|
|
357
|
+
session_data, headers=["Statistics", "Values"], tablefmt="outline")
|
|
339
358
|
|
|
340
359
|
# Print the formatted statistics
|
|
341
360
|
if self.verbose:
|
|
@@ -346,24 +365,10 @@ class Trade(RiskManagement):
|
|
|
346
365
|
if save:
|
|
347
366
|
today_date = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
348
367
|
# Create a dictionary with the statistics
|
|
349
|
-
statistics_dict = {
|
|
350
|
-
|
|
351
|
-
"Winning Trades": wins,
|
|
352
|
-
"Losing Trades": losses,
|
|
353
|
-
"Session Profit": f"{profit} {currency}",
|
|
354
|
-
"Total Fees": f"{total_fees} {currency}",
|
|
355
|
-
"Average Fees": f"{average_fee} {currency}",
|
|
356
|
-
"Net Profit": f"{net_profit} {currency}",
|
|
357
|
-
"Risk per Trade": f"{trade_risk} {currency}",
|
|
358
|
-
"Expected Profit per Trade": f"{expected_profit} {currency}",
|
|
359
|
-
"Risk Reward Ratio": self.rr,
|
|
360
|
-
"Win Rate": f"{win_rate}%",
|
|
361
|
-
"Sharpe Ratio": self.sharpe(),
|
|
362
|
-
"Trade Profitability": profitability,
|
|
363
|
-
}
|
|
368
|
+
statistics_dict = {item[0]: item[1] for item in session_data}
|
|
369
|
+
stats_df = pd.DataFrame(statistics_dict, index=[0])
|
|
364
370
|
# Create the directory if it doesn't exist
|
|
365
|
-
|
|
366
|
-
dir = f"{self.expert_name}_session_stats"
|
|
371
|
+
dir = dir or '.sessions'
|
|
367
372
|
os.makedirs(dir, exist_ok=True)
|
|
368
373
|
if '.' in self.symbol:
|
|
369
374
|
symbol = self.symbol.split('.')[0]
|
|
@@ -372,15 +377,7 @@ class Trade(RiskManagement):
|
|
|
372
377
|
|
|
373
378
|
filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
|
|
374
379
|
filepath = os.path.join(dir, filename)
|
|
375
|
-
|
|
376
|
-
# Updated code to write to CSV
|
|
377
|
-
with open(filepath, mode="w", newline='', encoding='utf-8') as csv_file:
|
|
378
|
-
writer = csv.writer(
|
|
379
|
-
csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL
|
|
380
|
-
)
|
|
381
|
-
writer.writerow(["Statistic", "Value"])
|
|
382
|
-
for stat, value in statistics_dict.items():
|
|
383
|
-
writer.writerow([stat, value])
|
|
380
|
+
stats_df.to_csv(filepath, index=False)
|
|
384
381
|
self.logger.info(f"Session statistics saved to {filepath}")
|
|
385
382
|
|
|
386
383
|
Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
|
|
@@ -388,6 +385,7 @@ class Trade(RiskManagement):
|
|
|
388
385
|
self,
|
|
389
386
|
action: Buys = 'BMKT',
|
|
390
387
|
price: Optional[float] = None,
|
|
388
|
+
stoplimit: Optional[float] = None,
|
|
391
389
|
mm: bool = True,
|
|
392
390
|
id: Optional[int] = None,
|
|
393
391
|
comment: Optional[str] = None
|
|
@@ -399,6 +397,8 @@ class Trade(RiskManagement):
|
|
|
399
397
|
action (str): `'BMKT'` for Market orders or `'BLMT',
|
|
400
398
|
'BSTP','BSTPLMT'` for pending orders
|
|
401
399
|
price (float): The price at which to open an order
|
|
400
|
+
stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
|
|
401
|
+
The pending order is not passed to the trading system until that moment
|
|
402
402
|
id (int): The strategy id or expert Id
|
|
403
403
|
mm (bool): Weither to put stop loss and tp or not
|
|
404
404
|
comment (str): The comment for the opening position
|
|
@@ -406,9 +406,11 @@ class Trade(RiskManagement):
|
|
|
406
406
|
Id = id if id is not None else self.expert_id
|
|
407
407
|
point = self.get_symbol_info(self.symbol).point
|
|
408
408
|
if action != 'BMKT':
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
409
|
+
if price is not None:
|
|
410
|
+
_price = price
|
|
411
|
+
else:
|
|
412
|
+
raise ValueError(
|
|
413
|
+
"You need to set a price for pending orders")
|
|
412
414
|
else:
|
|
413
415
|
_price = self.get_tick_info(self.symbol).ask
|
|
414
416
|
digits = self.get_symbol_info(self.symbol).digits
|
|
@@ -429,14 +431,23 @@ class Trade(RiskManagement):
|
|
|
429
431
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
430
432
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
431
433
|
}
|
|
432
|
-
|
|
433
|
-
request['sl'] = (_price - stop_loss * point)
|
|
434
|
-
request['tp'] = (_price + take_profit * point)
|
|
434
|
+
mm_price = _price
|
|
435
435
|
if action != 'BMKT':
|
|
436
436
|
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
437
437
|
request["type"] = self._order_type()[action][0]
|
|
438
|
-
if
|
|
439
|
-
|
|
438
|
+
if action == 'BSTPLMT':
|
|
439
|
+
if stoplimit is None:
|
|
440
|
+
raise ValueError(
|
|
441
|
+
"You need to set a stoplimit price for BSTPLMT orders")
|
|
442
|
+
if stoplimit > _price:
|
|
443
|
+
raise ValueError(
|
|
444
|
+
"Stoplimit price must be less than the price and greater than the current price")
|
|
445
|
+
request["stoplimit"] = stoplimit
|
|
446
|
+
mm_price = stoplimit
|
|
447
|
+
if mm:
|
|
448
|
+
request["sl"] = (mm_price - stop_loss * point)
|
|
449
|
+
request["tp"] = (mm_price + take_profit * point)
|
|
450
|
+
self.break_even(mm=mm, id=Id)
|
|
440
451
|
if self.check(comment):
|
|
441
452
|
self.request_result(_price, request, action),
|
|
442
453
|
|
|
@@ -458,6 +469,7 @@ class Trade(RiskManagement):
|
|
|
458
469
|
self,
|
|
459
470
|
action: Sells = 'SMKT',
|
|
460
471
|
price: Optional[float] = None,
|
|
472
|
+
stoplimit: Optional[float] = None,
|
|
461
473
|
mm: bool = True,
|
|
462
474
|
id: Optional[int] = None,
|
|
463
475
|
comment: Optional[str] = None
|
|
@@ -469,6 +481,8 @@ class Trade(RiskManagement):
|
|
|
469
481
|
action (str): `'SMKT'` for Market orders
|
|
470
482
|
or `'SLMT', 'SSTP','SSTPLMT'` for pending orders
|
|
471
483
|
price (float): The price at which to open an order
|
|
484
|
+
stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
|
|
485
|
+
The pending order is not passed to the trading system until that moment
|
|
472
486
|
id (int): The strategy id or expert Id
|
|
473
487
|
mm (bool): Weither to put stop loss and tp or not
|
|
474
488
|
comment (str): The comment for the closing position
|
|
@@ -476,9 +490,11 @@ class Trade(RiskManagement):
|
|
|
476
490
|
Id = id if id is not None else self.expert_id
|
|
477
491
|
point = self.get_symbol_info(self.symbol).point
|
|
478
492
|
if action != 'SMKT':
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
493
|
+
if price is not None:
|
|
494
|
+
_price = price
|
|
495
|
+
else:
|
|
496
|
+
raise ValueError(
|
|
497
|
+
"You need to set a price for pending orders")
|
|
482
498
|
else:
|
|
483
499
|
_price = self.get_tick_info(self.symbol).bid
|
|
484
500
|
digits = self.get_symbol_info(self.symbol).digits
|
|
@@ -499,24 +515,26 @@ class Trade(RiskManagement):
|
|
|
499
515
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
500
516
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
501
517
|
}
|
|
502
|
-
|
|
503
|
-
request["sl"] = (_price + stop_loss * point)
|
|
504
|
-
request["tp"] = (_price - take_profit * point)
|
|
518
|
+
mm_price = _price
|
|
505
519
|
if action != 'SMKT':
|
|
506
520
|
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
507
521
|
request["type"] = self._order_type()[action][0]
|
|
508
|
-
if
|
|
509
|
-
|
|
522
|
+
if action == 'SSTPLMT':
|
|
523
|
+
if stoplimit is None:
|
|
524
|
+
raise ValueError(
|
|
525
|
+
"You need to set a stoplimit price for SSTPLMT orders")
|
|
526
|
+
if stoplimit < _price:
|
|
527
|
+
raise ValueError(
|
|
528
|
+
"Stoplimit price must be greater than the price and less than the current price")
|
|
529
|
+
request["stoplimit"] = stoplimit
|
|
530
|
+
mm_price = stoplimit
|
|
531
|
+
if mm:
|
|
532
|
+
request["sl"] = (mm_price + stop_loss * point)
|
|
533
|
+
request["tp"] = (mm_price - take_profit * point)
|
|
534
|
+
self.break_even(mm=mm, id=Id)
|
|
510
535
|
if self.check(comment):
|
|
511
536
|
self.request_result(_price, request, action)
|
|
512
537
|
|
|
513
|
-
def _risk_free(self):
|
|
514
|
-
max_trade = self.max_trade()
|
|
515
|
-
loss_trades = self.get_stats()[0]['loss_trades']
|
|
516
|
-
if loss_trades >= max_trade:
|
|
517
|
-
return False
|
|
518
|
-
return True
|
|
519
|
-
|
|
520
538
|
def check(self, comment):
|
|
521
539
|
"""
|
|
522
540
|
Verify if all conditions for taking a position are valide,
|
|
@@ -535,16 +553,14 @@ class Trade(RiskManagement):
|
|
|
535
553
|
self.logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
|
|
536
554
|
self._check(comment)
|
|
537
555
|
return False
|
|
538
|
-
elif not self._risk_free():
|
|
539
|
-
self.logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
|
|
540
|
-
self._check(comment)
|
|
541
|
-
return False
|
|
542
556
|
elif self.profit_target():
|
|
543
557
|
self._check(f'Profit target Reached !!! SYMBOL={self.symbol}')
|
|
544
558
|
return True
|
|
545
559
|
|
|
546
560
|
def _check(self, txt: str = ""):
|
|
547
|
-
if self.positive_profit(
|
|
561
|
+
if (self.positive_profit(id=self.expert_id)
|
|
562
|
+
or self.get_current_positions() is None
|
|
563
|
+
):
|
|
548
564
|
self.close_positions(position_type='all')
|
|
549
565
|
self.logger.info(txt)
|
|
550
566
|
time.sleep(5)
|
|
@@ -571,7 +587,7 @@ class Trade(RiskManagement):
|
|
|
571
587
|
pos = self._order_type()[type][1]
|
|
572
588
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
573
589
|
try:
|
|
574
|
-
|
|
590
|
+
check = self.check_order(request)
|
|
575
591
|
result = self.send_order(request)
|
|
576
592
|
except Exception as e:
|
|
577
593
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -587,7 +603,7 @@ class Trade(RiskManagement):
|
|
|
587
603
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
588
604
|
time.sleep(1)
|
|
589
605
|
try:
|
|
590
|
-
|
|
606
|
+
check = self.check_order(request)
|
|
591
607
|
result = self.send_order(request)
|
|
592
608
|
except Exception as e:
|
|
593
609
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -636,6 +652,7 @@ class Trade(RiskManagement):
|
|
|
636
652
|
self,
|
|
637
653
|
action: Buys | Sells,
|
|
638
654
|
price: Optional[float] = None,
|
|
655
|
+
stoplimit: Optional[float] = None,
|
|
639
656
|
id: Optional[int] = None,
|
|
640
657
|
mm: bool = True,
|
|
641
658
|
comment: Optional[str] = None
|
|
@@ -646,6 +663,9 @@ class Trade(RiskManagement):
|
|
|
646
663
|
Args:
|
|
647
664
|
action (str): (`'BMKT'`, `'SMKT'`) for Market orders
|
|
648
665
|
or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
|
|
666
|
+
price (float): The price at which to open an order
|
|
667
|
+
stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
|
|
668
|
+
The pending order is not passed to the trading system until that moment
|
|
649
669
|
id (int): The strategy id or expert Id
|
|
650
670
|
mm (bool): Weither to put stop loss and tp or not
|
|
651
671
|
comment (str): The comment for the closing position
|
|
@@ -654,43 +674,43 @@ class Trade(RiskManagement):
|
|
|
654
674
|
SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
655
675
|
if action in BUYS:
|
|
656
676
|
self.open_buy_position(
|
|
657
|
-
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
677
|
+
action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
658
678
|
elif action in SELLS:
|
|
659
679
|
self.open_sell_position(
|
|
660
|
-
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
680
|
+
action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
661
681
|
else:
|
|
662
682
|
raise ValueError(f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
|
|
663
683
|
|
|
664
684
|
@property
|
|
665
|
-
def
|
|
685
|
+
def orders(self):
|
|
666
686
|
""" Return all opened order's tickets"""
|
|
667
687
|
if len(self.opened_orders) != 0:
|
|
668
688
|
return self.opened_orders
|
|
669
689
|
return None
|
|
670
690
|
|
|
671
691
|
@property
|
|
672
|
-
def
|
|
692
|
+
def positions(self):
|
|
673
693
|
"""Return all opened position's tickets"""
|
|
674
694
|
if len(self.opened_positions) != 0:
|
|
675
695
|
return self.opened_positions
|
|
676
696
|
return None
|
|
677
697
|
|
|
678
698
|
@property
|
|
679
|
-
def
|
|
699
|
+
def buypos(self):
|
|
680
700
|
"""Return all buy opened position's tickets"""
|
|
681
701
|
if len(self.buy_positions) != 0:
|
|
682
702
|
return self.buy_positions
|
|
683
703
|
return None
|
|
684
704
|
|
|
685
705
|
@property
|
|
686
|
-
def
|
|
706
|
+
def sellpos(self):
|
|
687
707
|
"""Return all sell opened position's tickets"""
|
|
688
708
|
if len(self.sell_positions) != 0:
|
|
689
709
|
return self.sell_positions
|
|
690
710
|
return None
|
|
691
711
|
|
|
692
712
|
@property
|
|
693
|
-
def
|
|
713
|
+
def bepos(self):
|
|
694
714
|
"""Return All positon's tickets
|
|
695
715
|
for which a break even has been set"""
|
|
696
716
|
if len(self.break_even_status) != 0:
|
|
@@ -707,8 +727,14 @@ class Trade(RiskManagement):
|
|
|
707
727
|
|
|
708
728
|
Args:
|
|
709
729
|
id (int): The strategy id or expert Id
|
|
710
|
-
filter_type (str): Filter type
|
|
730
|
+
filter_type (str): Filter type to apply on the tickets,
|
|
711
731
|
- `orders` are current open orders
|
|
732
|
+
- `buy_stops` are current buy stop orders
|
|
733
|
+
- `sell_stops` are current sell stop orders
|
|
734
|
+
- `buy_limits` are current buy limit orders
|
|
735
|
+
- `sell_limits` are current sell limit orders
|
|
736
|
+
- `buy_stop_limits` are current buy stop limit orders
|
|
737
|
+
- `sell_stop_limits` are current sell stop limit orders
|
|
712
738
|
- `positions` are all current open positions
|
|
713
739
|
- `buys` and `sells` are current buy or sell open positions
|
|
714
740
|
- `profitables` are current open position that have a profit greater than a threshold
|
|
@@ -721,8 +747,9 @@ class Trade(RiskManagement):
|
|
|
721
747
|
or None if no tickets match the criteria.
|
|
722
748
|
"""
|
|
723
749
|
Id = id if id is not None else self.expert_id
|
|
750
|
+
POSITIONS = ['positions', 'buys', 'sells', 'profitables', 'losings']
|
|
724
751
|
|
|
725
|
-
if filter_type
|
|
752
|
+
if filter_type not in POSITIONS:
|
|
726
753
|
items = self.get_orders(symbol=self.symbol)
|
|
727
754
|
else:
|
|
728
755
|
items = self.get_positions(symbol=self.symbol)
|
|
@@ -736,18 +763,48 @@ class Trade(RiskManagement):
|
|
|
736
763
|
continue
|
|
737
764
|
if filter_type == 'sells' and item.type != 1:
|
|
738
765
|
continue
|
|
766
|
+
if filter_type == 'losings' and item.profit > 0:
|
|
767
|
+
continue
|
|
739
768
|
if filter_type == 'profitables' and not self.win_trade(item, th=th):
|
|
740
769
|
continue
|
|
741
|
-
if filter_type == '
|
|
770
|
+
if filter_type == 'buy_stops' and item.type != self._order_type()['BSTP'][0]:
|
|
771
|
+
continue
|
|
772
|
+
if filter_type == 'sell_stops' and item.type != self._order_type()['SSTP'][0]:
|
|
773
|
+
continue
|
|
774
|
+
if filter_type == 'buy_limits' and item.type != self._order_type()['BLMT'][0]:
|
|
775
|
+
continue
|
|
776
|
+
if filter_type == 'sell_limits' and item.type != self._order_type()['SLMT'][0]:
|
|
777
|
+
continue
|
|
778
|
+
if filter_type == 'buy_stop_limits' and item.type != self._order_type()['BSTPLMT'][0]:
|
|
779
|
+
continue
|
|
780
|
+
if filter_type == 'sell_stop_limits' and item.type != self._order_type()['SSTPLMT'][0]:
|
|
742
781
|
continue
|
|
743
782
|
filtered_tickets.append(item.ticket)
|
|
744
783
|
return filtered_tickets if filtered_tickets else None
|
|
745
784
|
return None
|
|
746
785
|
|
|
747
|
-
def
|
|
786
|
+
def get_current_orders(self, id: Optional[int] = None) -> List[int] | None:
|
|
748
787
|
return self.get_filtered_tickets(id=id, filter_type='orders')
|
|
749
788
|
|
|
750
|
-
def
|
|
789
|
+
def get_current_buy_stops(self, id: Optional[int] = None) -> List[int] | None:
|
|
790
|
+
return self.get_filtered_tickets(id=id, filter_type='buy_stops')
|
|
791
|
+
|
|
792
|
+
def get_current_sell_stops(self, id: Optional[int] = None) -> List[int] | None:
|
|
793
|
+
return self.get_filtered_tickets(id=id, filter_type='sell_stops')
|
|
794
|
+
|
|
795
|
+
def get_current_buy_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
796
|
+
return self.get_filtered_tickets(id=id, filter_type='buy_limits')
|
|
797
|
+
|
|
798
|
+
def get_current_sell_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
799
|
+
return self.get_filtered_tickets(id=id, filter_type='sell_limits')
|
|
800
|
+
|
|
801
|
+
def get_current_buy_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
802
|
+
return self.get_filtered_tickets(id=id, filter_type='buy_stop_limits')
|
|
803
|
+
|
|
804
|
+
def get_current_sell_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
805
|
+
return self.get_filtered_tickets(id=id, filter_type='sell_stop_limits')
|
|
806
|
+
|
|
807
|
+
def get_current_positions(self, id: Optional[int] = None) -> List[int] | None:
|
|
751
808
|
return self.get_filtered_tickets(id=id, filter_type='positions')
|
|
752
809
|
|
|
753
810
|
def get_current_profitables(self, id: Optional[int] = None, th=None) -> List[int] | None:
|
|
@@ -862,7 +919,7 @@ class Trade(RiskManagement):
|
|
|
862
919
|
# This ensures that the position rich the minimum points required
|
|
863
920
|
# before the trail can be set
|
|
864
921
|
new_be = trail_after_points - be
|
|
865
|
-
self.trail_after_points
|
|
922
|
+
self.trail_after_points.append(position.ticket)
|
|
866
923
|
new_be_points = self.break_even_points[position.ticket] + new_be
|
|
867
924
|
favorable_move = float(points/point) >= new_be_points
|
|
868
925
|
if favorable_move:
|
|
@@ -874,10 +931,14 @@ class Trade(RiskManagement):
|
|
|
874
931
|
# This level validate the favorable move of the price
|
|
875
932
|
new_level = round(position.price_open + (new_be_points * point), digits)
|
|
876
933
|
# This price is set away from the current price by the trail_points
|
|
877
|
-
new_price = round(position.price_current - (trail_points * point), digits)
|
|
934
|
+
new_price = round(position.price_current - (trail_points * point), digits)
|
|
935
|
+
if new_price < position.sl:
|
|
936
|
+
new_price = position.sl
|
|
878
937
|
elif position.type == 1:
|
|
879
938
|
new_level = round(position.price_open - (new_be_points * point), digits)
|
|
880
|
-
new_price = round(position.price_current + (trail_points * point), digits)
|
|
939
|
+
new_price = round(position.price_current + (trail_points * point), digits)
|
|
940
|
+
if new_price > position.sl:
|
|
941
|
+
new_price = position.sl
|
|
881
942
|
self.set_break_even(
|
|
882
943
|
position, be, price=new_price, level=new_level
|
|
883
944
|
)
|
|
@@ -922,7 +983,7 @@ class Trade(RiskManagement):
|
|
|
922
983
|
"sl": round(_price, digits),
|
|
923
984
|
"tp": position.tp
|
|
924
985
|
}
|
|
925
|
-
self.
|
|
986
|
+
self.break_even_request(
|
|
926
987
|
position.ticket, round(_price, digits), request)
|
|
927
988
|
# If Sell
|
|
928
989
|
elif position.type == 1 and position.price_current < position.price_open:
|
|
@@ -942,10 +1003,10 @@ class Trade(RiskManagement):
|
|
|
942
1003
|
"sl": round(_price, digits),
|
|
943
1004
|
"tp": position.tp
|
|
944
1005
|
}
|
|
945
|
-
self.
|
|
1006
|
+
self.break_even_request(
|
|
946
1007
|
position.ticket, round(_price, digits), request)
|
|
947
1008
|
|
|
948
|
-
def
|
|
1009
|
+
def break_even_request(self, tiket, price, request):
|
|
949
1010
|
"""
|
|
950
1011
|
Send a request to set the stop loss to break even for a given trading position.
|
|
951
1012
|
|
|
@@ -1000,6 +1061,7 @@ class Trade(RiskManagement):
|
|
|
1000
1061
|
wen it is closed before be level , tp or sl.
|
|
1001
1062
|
|
|
1002
1063
|
Args:
|
|
1064
|
+
position (TradePosition): The trading position to check.
|
|
1003
1065
|
th (int): The minimum profit for a position in point
|
|
1004
1066
|
"""
|
|
1005
1067
|
size = self.get_symbol_info(self.symbol).trade_tick_size
|
|
@@ -1050,6 +1112,75 @@ class Trade(RiskManagement):
|
|
|
1050
1112
|
return True
|
|
1051
1113
|
return False
|
|
1052
1114
|
|
|
1115
|
+
def close_request(self, request: dict, type: str):
|
|
1116
|
+
"""
|
|
1117
|
+
Close a trading order or position
|
|
1118
|
+
|
|
1119
|
+
Args:
|
|
1120
|
+
request (dict): The request to close a trading order or position
|
|
1121
|
+
type (str): Type of the request ('order', 'position')
|
|
1122
|
+
"""
|
|
1123
|
+
ticket = request[type]
|
|
1124
|
+
addtionnal = f", SYMBOL={self.symbol}"
|
|
1125
|
+
try:
|
|
1126
|
+
check_result = self.check_order(request)
|
|
1127
|
+
result = self.send_order(request)
|
|
1128
|
+
except Exception as e:
|
|
1129
|
+
print(f"{self.current_datetime()} -", end=' ')
|
|
1130
|
+
trade_retcode_message(
|
|
1131
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1132
|
+
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1133
|
+
msg = trade_retcode_message(result.retcode)
|
|
1134
|
+
self.logger.error(
|
|
1135
|
+
f"Closing Order Request, {type.capitalize()}: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
1136
|
+
tries = 0
|
|
1137
|
+
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
1138
|
+
time.sleep(1)
|
|
1139
|
+
try:
|
|
1140
|
+
check_result = self.check_order(request)
|
|
1141
|
+
result = self.send_order(request)
|
|
1142
|
+
except Exception as e:
|
|
1143
|
+
print(f"{self.current_datetime()} -", end=' ')
|
|
1144
|
+
trade_retcode_message(
|
|
1145
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1146
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1147
|
+
break
|
|
1148
|
+
tries += 1
|
|
1149
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1150
|
+
msg = trade_retcode_message(result.retcode)
|
|
1151
|
+
self.logger.info(
|
|
1152
|
+
f"Closing Order {msg}{addtionnal}")
|
|
1153
|
+
info = (
|
|
1154
|
+
f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol}, Price: @{request.get('price', 0.0)}")
|
|
1155
|
+
self.logger.info(info)
|
|
1156
|
+
return True
|
|
1157
|
+
else:
|
|
1158
|
+
return False
|
|
1159
|
+
|
|
1160
|
+
def close_order(self,
|
|
1161
|
+
ticket: int,
|
|
1162
|
+
id: Optional[int] = None,
|
|
1163
|
+
comment: Optional[str] = None):
|
|
1164
|
+
"""
|
|
1165
|
+
Close an open order by it ticket
|
|
1166
|
+
|
|
1167
|
+
Args:
|
|
1168
|
+
ticket (int): Order ticket to close (e.g TradeOrder.ticket)
|
|
1169
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1170
|
+
comment (str): Comment for the closing position
|
|
1171
|
+
|
|
1172
|
+
Returns:
|
|
1173
|
+
- True if order closed, False otherwise
|
|
1174
|
+
"""
|
|
1175
|
+
request = {
|
|
1176
|
+
"action": Mt5.TRADE_ACTION_REMOVE,
|
|
1177
|
+
"symbol": self.symbol,
|
|
1178
|
+
"order": ticket,
|
|
1179
|
+
"magic": id if id is not None else self.expert_id,
|
|
1180
|
+
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
1181
|
+
}
|
|
1182
|
+
return self.close_request(request, type="order")
|
|
1183
|
+
|
|
1053
1184
|
def close_position(self,
|
|
1054
1185
|
ticket: int,
|
|
1055
1186
|
id: Optional[int] = None,
|
|
@@ -1096,41 +1227,77 @@ class Trade(RiskManagement):
|
|
|
1096
1227
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
1097
1228
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
1098
1229
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
if
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1230
|
+
return self.close_request(request, type="position")
|
|
1231
|
+
|
|
1232
|
+
def bulk_close(self,
|
|
1233
|
+
tickets: List,
|
|
1234
|
+
tikets_type: Literal["positions", "orders"],
|
|
1235
|
+
close_func: Callable,
|
|
1236
|
+
order_type: str,
|
|
1237
|
+
id: Optional[int] = None,
|
|
1238
|
+
comment: Optional[str] = None):
|
|
1239
|
+
|
|
1240
|
+
"""
|
|
1241
|
+
Close multiple orders or positions at once.
|
|
1242
|
+
|
|
1243
|
+
Args:
|
|
1244
|
+
tickets (List): List of tickets to close
|
|
1245
|
+
tikets_type (str): Type of tickets to close ('positions', 'orders')
|
|
1246
|
+
close_func (Callable): The function to close the tickets
|
|
1247
|
+
order_type (str): Type of orders or positions to close
|
|
1248
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1249
|
+
comment (str): Comment for the closing position
|
|
1250
|
+
"""
|
|
1251
|
+
if order_type == 'all':
|
|
1252
|
+
order_type = 'open'
|
|
1253
|
+
if len(tickets) > 0:
|
|
1254
|
+
for ticket in tickets.copy():
|
|
1255
|
+
if close_func(ticket, id=id, comment=comment):
|
|
1256
|
+
tickets.remove(ticket)
|
|
1257
|
+
time.sleep(1)
|
|
1258
|
+
|
|
1259
|
+
if len(tickets) == 0:
|
|
1260
|
+
self.logger.info(
|
|
1261
|
+
f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}.")
|
|
1262
|
+
else:
|
|
1263
|
+
self.logger.info(
|
|
1264
|
+
f"{len(tickets)} {order_type.upper()} {tikets_type.upper()} not closed, SYMBOL={self.symbol}")
|
|
1265
|
+
else:
|
|
1266
|
+
self.logger.info(
|
|
1267
|
+
f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}.")
|
|
1268
|
+
|
|
1269
|
+
Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
|
|
1270
|
+
"sell_limits", "buy_stop_limits", "sell_stop_limits"]
|
|
1271
|
+
def close_orders(self,
|
|
1272
|
+
order_type: Orders,
|
|
1273
|
+
id: Optional[int] = None,
|
|
1274
|
+
comment: Optional[str] = None):
|
|
1275
|
+
"""
|
|
1276
|
+
Args:
|
|
1277
|
+
order_type (str): Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
|
|
1278
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1279
|
+
comment (str): Comment for the closing position
|
|
1280
|
+
"""
|
|
1281
|
+
id = id if id is not None else self.expert_id
|
|
1282
|
+
if order_type == "all":
|
|
1283
|
+
orders = self.get_current_orders(id=id)
|
|
1284
|
+
elif order_type == "buy_stops":
|
|
1285
|
+
orders = self.get_current_buy_stops(id=id)
|
|
1286
|
+
elif order_type == "sell_stops":
|
|
1287
|
+
orders = self.get_current_sell_stops(id=id)
|
|
1288
|
+
elif order_type == "buy_limits":
|
|
1289
|
+
orders = self.get_current_buy_limits(id=id)
|
|
1290
|
+
elif order_type == "sell_limits":
|
|
1291
|
+
orders = self.get_current_sell_limits(id=id)
|
|
1292
|
+
elif order_type == "buy_stop_limits":
|
|
1293
|
+
orders = self.get_current_buy_stop_limits(id=id)
|
|
1294
|
+
elif order_type == "sell_stop_limits":
|
|
1295
|
+
orders = self.get_current_sell_stop_limits(id=id)
|
|
1296
|
+
else:
|
|
1297
|
+
self.logger.error(f"Invalid order type: {order_type}")
|
|
1298
|
+
return
|
|
1299
|
+
self.bulk_close(
|
|
1300
|
+
orders, "orders", self.close_order, order_type, id=id, comment=comment)
|
|
1134
1301
|
|
|
1135
1302
|
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
1136
1303
|
def close_positions(
|
|
@@ -1144,8 +1311,9 @@ class Trade(RiskManagement):
|
|
|
1144
1311
|
id (int): The unique ID of the Expert or Strategy
|
|
1145
1312
|
comment (str): Comment for the closing position
|
|
1146
1313
|
"""
|
|
1314
|
+
id = id if id is not None else self.expert_id
|
|
1147
1315
|
if position_type == "all":
|
|
1148
|
-
positions = self.
|
|
1316
|
+
positions = self.get_current_positions(id=id)
|
|
1149
1317
|
elif position_type == "buy":
|
|
1150
1318
|
positions = self.get_current_buys(id=id)
|
|
1151
1319
|
elif position_type == "sell":
|
|
@@ -1157,35 +1325,8 @@ class Trade(RiskManagement):
|
|
|
1157
1325
|
else:
|
|
1158
1326
|
self.logger.error(f"Invalid position type: {position_type}")
|
|
1159
1327
|
return
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
if position_type == 'all':
|
|
1163
|
-
tickets = [position.ticket for position in positions if position.magic == id]
|
|
1164
|
-
else:
|
|
1165
|
-
tickets = positions
|
|
1166
|
-
else:
|
|
1167
|
-
tickets = []
|
|
1168
|
-
|
|
1169
|
-
if position_type == 'all':
|
|
1170
|
-
pos_type = 'open'
|
|
1171
|
-
else:
|
|
1172
|
-
pos_type = position_type
|
|
1173
|
-
|
|
1174
|
-
if len(tickets) != 0:
|
|
1175
|
-
for ticket in tickets.copy():
|
|
1176
|
-
if self.close_position(ticket, id=id, comment=comment):
|
|
1177
|
-
tickets.remove(ticket)
|
|
1178
|
-
time.sleep(1)
|
|
1179
|
-
|
|
1180
|
-
if len(tickets) == 0:
|
|
1181
|
-
self.logger.info(
|
|
1182
|
-
f"ALL {pos_type.upper()} Positions closed, SYMBOL={self.symbol}.")
|
|
1183
|
-
else:
|
|
1184
|
-
self.logger.info(
|
|
1185
|
-
f"{len(tickets)} {pos_type.upper()} Positions not closed, SYMBOL={self.symbol}")
|
|
1186
|
-
else:
|
|
1187
|
-
self.logger.info(
|
|
1188
|
-
f"No {pos_type.upper()} Positions to close, SYMBOL={self.symbol}.")
|
|
1328
|
+
self.bulk_close(
|
|
1329
|
+
positions, "positions", self.close_position, position_type, id=id, comment=comment)
|
|
1189
1330
|
|
|
1190
1331
|
def get_stats(self) -> Tuple[Dict[str, Any]]:
|
|
1191
1332
|
"""
|
|
@@ -1342,30 +1483,77 @@ class Trade(RiskManagement):
|
|
|
1342
1483
|
def create_trade_instance(
|
|
1343
1484
|
symbols: List[str],
|
|
1344
1485
|
params: Dict[str, Any],
|
|
1345
|
-
|
|
1486
|
+
daily_risk: Optional[Dict[str, float]] = None,
|
|
1487
|
+
max_risk: Optional[Dict[str, float]] = None,
|
|
1488
|
+
pchange_sl: Optional[Dict[str, float] | float] = None,
|
|
1489
|
+
) -> Dict[str, Trade]:
|
|
1346
1490
|
"""
|
|
1347
1491
|
Creates Trade instances for each symbol provided.
|
|
1348
1492
|
|
|
1349
1493
|
Args:
|
|
1350
1494
|
symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
|
|
1351
1495
|
params: A dictionary containing parameters for the Trade instance.
|
|
1496
|
+
daily_risk: A dictionary containing daily risk weight for each symbol.
|
|
1497
|
+
max_risk: A dictionary containing maximum risk weight for each symbol.
|
|
1352
1498
|
|
|
1353
1499
|
Returns:
|
|
1354
1500
|
A dictionary where keys are symbols and values are corresponding Trade instances.
|
|
1355
1501
|
|
|
1356
1502
|
Raises:
|
|
1357
1503
|
ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
|
|
1504
|
+
|
|
1505
|
+
Note:
|
|
1506
|
+
`daily_risk` and `max_risk` can be used to manage the risk of each symbol
|
|
1507
|
+
based on the importance of the symbol in the portfolio or strategy.
|
|
1358
1508
|
"""
|
|
1359
|
-
|
|
1509
|
+
logger = params.get('logger', None)
|
|
1510
|
+
trade_instances = {}
|
|
1360
1511
|
if not symbols:
|
|
1361
1512
|
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1513
|
+
if not params:
|
|
1514
|
+
raise ValueError("The 'params' dictionary cannot be empty.")
|
|
1515
|
+
|
|
1516
|
+
if daily_risk is not None:
|
|
1517
|
+
for symbol in symbols:
|
|
1518
|
+
if symbol not in daily_risk:
|
|
1519
|
+
raise ValueError(f"Missing daily risk weight for symbol '{symbol}'.")
|
|
1520
|
+
if max_risk is not None:
|
|
1521
|
+
for symbol in symbols:
|
|
1522
|
+
if symbol not in max_risk:
|
|
1523
|
+
raise ValueError(f"Missing maximum risk percentage for symbol '{symbol}'.")
|
|
1524
|
+
if pchange_sl is not None:
|
|
1525
|
+
if isinstance(pchange_sl, dict):
|
|
1526
|
+
for symbol in symbols:
|
|
1527
|
+
if symbol not in pchange_sl:
|
|
1528
|
+
raise ValueError(f"Missing percentage change for symbol '{symbol}'.")
|
|
1529
|
+
|
|
1362
1530
|
for symbol in symbols:
|
|
1363
1531
|
try:
|
|
1364
|
-
|
|
1532
|
+
params['symbol'] = symbol
|
|
1533
|
+
params['pchange_sl'] = (
|
|
1534
|
+
pchange_sl[symbol] if pchange_sl is not None
|
|
1535
|
+
and isinstance(pchange_sl, dict) else
|
|
1536
|
+
pchange_sl if pchange_sl is not None
|
|
1537
|
+
and isinstance(pchange_sl, (int, float)) else
|
|
1538
|
+
params['pchange_sl'] if 'pchange_sl' in params else None
|
|
1539
|
+
)
|
|
1540
|
+
params['daily_risk'] = (
|
|
1541
|
+
daily_risk[symbol] if daily_risk is not None else
|
|
1542
|
+
params['daily_risk'] if 'daily_risk' in params else None
|
|
1543
|
+
)
|
|
1544
|
+
params['max_risk'] = (
|
|
1545
|
+
max_risk[symbol] if max_risk is not None else
|
|
1546
|
+
params['max_risk'] if 'max_risk' in params else 10.0
|
|
1547
|
+
)
|
|
1548
|
+
trade_instances[symbol] = Trade(**params)
|
|
1365
1549
|
except Exception as e:
|
|
1366
1550
|
logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1367
|
-
|
|
1551
|
+
|
|
1552
|
+
if len(trade_instances) != len(symbols):
|
|
1368
1553
|
for symbol in symbols:
|
|
1369
|
-
if symbol not in
|
|
1370
|
-
logger
|
|
1371
|
-
|
|
1554
|
+
if symbol not in trade_instances:
|
|
1555
|
+
if logger is not None and isinstance(logger, Logger):
|
|
1556
|
+
logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1557
|
+
else:
|
|
1558
|
+
raise ValueError(f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1559
|
+
return trade_instances
|