bbstrader 0.1.9__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 +348 -53
- bbstrader/metatrader/rates.py +232 -27
- bbstrader/metatrader/risk.py +34 -23
- bbstrader/metatrader/trade.py +321 -165
- 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 +268 -164
- bbstrader/trading/scripts.py +57 -0
- bbstrader/trading/strategies.py +41 -65
- bbstrader/tseries.py +274 -39
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/METADATA +11 -3
- bbstrader-0.1.91.dist-info/RECORD +31 -0
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/WHEEL +1 -1
- bbstrader-0.1.9.dist-info/RECORD +0 -26
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.9.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
|
"""
|
|
@@ -142,12 +166,12 @@ class Trade(RiskManagement):
|
|
|
142
166
|
":")
|
|
143
167
|
self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
|
|
144
168
|
|
|
145
|
-
self.buy_positions
|
|
146
|
-
self.sell_positions
|
|
147
|
-
self.opened_positions
|
|
148
|
-
self.opened_orders
|
|
149
|
-
self.break_even_status
|
|
150
|
-
self.break_even_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 = {}
|
|
151
175
|
self.trail_after_points = []
|
|
152
176
|
|
|
153
177
|
self.initialize()
|
|
@@ -165,7 +189,9 @@ class Trade(RiskManagement):
|
|
|
165
189
|
def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
|
|
166
190
|
"""Get the logger object"""
|
|
167
191
|
if isinstance(logger, str):
|
|
168
|
-
|
|
192
|
+
log_path = BBSTRADER_DIR / 'logs'
|
|
193
|
+
log_path.mkdir(exist_ok=True)
|
|
194
|
+
return config_logger(f'{log_path}/{logger}', consol_log)
|
|
169
195
|
return logger
|
|
170
196
|
|
|
171
197
|
def initialize(self):
|
|
@@ -302,14 +328,10 @@ class Trade(RiskManagement):
|
|
|
302
328
|
"""
|
|
303
329
|
stats, additional_stats = self.get_stats()
|
|
304
330
|
|
|
305
|
-
deals = stats["deals"]
|
|
306
|
-
wins = stats["win_trades"]
|
|
307
|
-
losses = stats["loss_trades"]
|
|
308
331
|
profit = round(stats["profit"], 2)
|
|
309
332
|
win_rate = stats["win_rate"]
|
|
310
333
|
total_fees = round(stats["total_fees"], 3)
|
|
311
334
|
average_fee = round(stats["average_fee"], 3)
|
|
312
|
-
profitability = additional_stats["profitability"]
|
|
313
335
|
currency = self.get_account_info().currency
|
|
314
336
|
net_profit = round((profit + total_fees), 2)
|
|
315
337
|
trade_risk = round(self.get_currency_risk() * -1, 2)
|
|
@@ -317,9 +339,9 @@ class Trade(RiskManagement):
|
|
|
317
339
|
|
|
318
340
|
# Formatting the statistics output
|
|
319
341
|
session_data = [
|
|
320
|
-
["Total Trades", deals],
|
|
321
|
-
["Winning Trades",
|
|
322
|
-
["Losing Trades",
|
|
342
|
+
["Total Trades", stats["deals"]],
|
|
343
|
+
["Winning Trades", stats["win_trades"]],
|
|
344
|
+
["Losing Trades", stats["loss_trades"]],
|
|
323
345
|
["Session Profit", f"{profit} {currency}"],
|
|
324
346
|
["Total Fees", f"{total_fees} {currency}"],
|
|
325
347
|
["Average Fees", f"{average_fee} {currency}"],
|
|
@@ -329,9 +351,10 @@ class Trade(RiskManagement):
|
|
|
329
351
|
["Risk Reward Ratio", self.rr],
|
|
330
352
|
["Win Rate", f"{win_rate}%"],
|
|
331
353
|
["Sharpe Ratio", self.sharpe()],
|
|
332
|
-
["Trade Profitability", profitability],
|
|
354
|
+
["Trade Profitability", additional_stats["profitability"]],
|
|
333
355
|
]
|
|
334
|
-
session_table = tabulate(
|
|
356
|
+
session_table = tabulate(
|
|
357
|
+
session_data, headers=["Statistics", "Values"], tablefmt="outline")
|
|
335
358
|
|
|
336
359
|
# Print the formatted statistics
|
|
337
360
|
if self.verbose:
|
|
@@ -342,24 +365,10 @@ class Trade(RiskManagement):
|
|
|
342
365
|
if save:
|
|
343
366
|
today_date = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
344
367
|
# Create a dictionary with the statistics
|
|
345
|
-
statistics_dict = {
|
|
346
|
-
|
|
347
|
-
"Winning Trades": wins,
|
|
348
|
-
"Losing Trades": losses,
|
|
349
|
-
"Session Profit": f"{profit} {currency}",
|
|
350
|
-
"Total Fees": f"{total_fees} {currency}",
|
|
351
|
-
"Average Fees": f"{average_fee} {currency}",
|
|
352
|
-
"Net Profit": f"{net_profit} {currency}",
|
|
353
|
-
"Risk per Trade": f"{trade_risk} {currency}",
|
|
354
|
-
"Expected Profit per Trade": f"{expected_profit} {currency}",
|
|
355
|
-
"Risk Reward Ratio": self.rr,
|
|
356
|
-
"Win Rate": f"{win_rate}%",
|
|
357
|
-
"Sharpe Ratio": self.sharpe(),
|
|
358
|
-
"Trade Profitability": profitability,
|
|
359
|
-
}
|
|
368
|
+
statistics_dict = {item[0]: item[1] for item in session_data}
|
|
369
|
+
stats_df = pd.DataFrame(statistics_dict, index=[0])
|
|
360
370
|
# Create the directory if it doesn't exist
|
|
361
|
-
|
|
362
|
-
dir = f".{self.expert_name}_session_stats"
|
|
371
|
+
dir = dir or '.sessions'
|
|
363
372
|
os.makedirs(dir, exist_ok=True)
|
|
364
373
|
if '.' in self.symbol:
|
|
365
374
|
symbol = self.symbol.split('.')[0]
|
|
@@ -368,15 +377,7 @@ class Trade(RiskManagement):
|
|
|
368
377
|
|
|
369
378
|
filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
|
|
370
379
|
filepath = os.path.join(dir, filename)
|
|
371
|
-
|
|
372
|
-
# Updated code to write to CSV
|
|
373
|
-
with open(filepath, mode="w", newline='', encoding='utf-8') as csv_file:
|
|
374
|
-
writer = csv.writer(
|
|
375
|
-
csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL
|
|
376
|
-
)
|
|
377
|
-
writer.writerow(["Statistic", "Value"])
|
|
378
|
-
for stat, value in statistics_dict.items():
|
|
379
|
-
writer.writerow([stat, value])
|
|
380
|
+
stats_df.to_csv(filepath, index=False)
|
|
380
381
|
self.logger.info(f"Session statistics saved to {filepath}")
|
|
381
382
|
|
|
382
383
|
Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
|
|
@@ -384,6 +385,7 @@ class Trade(RiskManagement):
|
|
|
384
385
|
self,
|
|
385
386
|
action: Buys = 'BMKT',
|
|
386
387
|
price: Optional[float] = None,
|
|
388
|
+
stoplimit: Optional[float] = None,
|
|
387
389
|
mm: bool = True,
|
|
388
390
|
id: Optional[int] = None,
|
|
389
391
|
comment: Optional[str] = None
|
|
@@ -395,6 +397,8 @@ class Trade(RiskManagement):
|
|
|
395
397
|
action (str): `'BMKT'` for Market orders or `'BLMT',
|
|
396
398
|
'BSTP','BSTPLMT'` for pending orders
|
|
397
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
|
|
398
402
|
id (int): The strategy id or expert Id
|
|
399
403
|
mm (bool): Weither to put stop loss and tp or not
|
|
400
404
|
comment (str): The comment for the opening position
|
|
@@ -402,9 +406,11 @@ class Trade(RiskManagement):
|
|
|
402
406
|
Id = id if id is not None else self.expert_id
|
|
403
407
|
point = self.get_symbol_info(self.symbol).point
|
|
404
408
|
if action != 'BMKT':
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
409
|
+
if price is not None:
|
|
410
|
+
_price = price
|
|
411
|
+
else:
|
|
412
|
+
raise ValueError(
|
|
413
|
+
"You need to set a price for pending orders")
|
|
408
414
|
else:
|
|
409
415
|
_price = self.get_tick_info(self.symbol).ask
|
|
410
416
|
digits = self.get_symbol_info(self.symbol).digits
|
|
@@ -425,12 +431,22 @@ class Trade(RiskManagement):
|
|
|
425
431
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
426
432
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
427
433
|
}
|
|
428
|
-
|
|
429
|
-
request['sl'] = (_price - stop_loss * point)
|
|
430
|
-
request['tp'] = (_price + take_profit * point)
|
|
434
|
+
mm_price = _price
|
|
431
435
|
if action != 'BMKT':
|
|
432
436
|
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
433
437
|
request["type"] = self._order_type()[action][0]
|
|
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)
|
|
434
450
|
self.break_even(mm=mm, id=Id)
|
|
435
451
|
if self.check(comment):
|
|
436
452
|
self.request_result(_price, request, action),
|
|
@@ -453,6 +469,7 @@ class Trade(RiskManagement):
|
|
|
453
469
|
self,
|
|
454
470
|
action: Sells = 'SMKT',
|
|
455
471
|
price: Optional[float] = None,
|
|
472
|
+
stoplimit: Optional[float] = None,
|
|
456
473
|
mm: bool = True,
|
|
457
474
|
id: Optional[int] = None,
|
|
458
475
|
comment: Optional[str] = None
|
|
@@ -464,6 +481,8 @@ class Trade(RiskManagement):
|
|
|
464
481
|
action (str): `'SMKT'` for Market orders
|
|
465
482
|
or `'SLMT', 'SSTP','SSTPLMT'` for pending orders
|
|
466
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
|
|
467
486
|
id (int): The strategy id or expert Id
|
|
468
487
|
mm (bool): Weither to put stop loss and tp or not
|
|
469
488
|
comment (str): The comment for the closing position
|
|
@@ -471,9 +490,11 @@ class Trade(RiskManagement):
|
|
|
471
490
|
Id = id if id is not None else self.expert_id
|
|
472
491
|
point = self.get_symbol_info(self.symbol).point
|
|
473
492
|
if action != 'SMKT':
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
493
|
+
if price is not None:
|
|
494
|
+
_price = price
|
|
495
|
+
else:
|
|
496
|
+
raise ValueError(
|
|
497
|
+
"You need to set a price for pending orders")
|
|
477
498
|
else:
|
|
478
499
|
_price = self.get_tick_info(self.symbol).bid
|
|
479
500
|
digits = self.get_symbol_info(self.symbol).digits
|
|
@@ -494,23 +515,26 @@ class Trade(RiskManagement):
|
|
|
494
515
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
495
516
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
496
517
|
}
|
|
497
|
-
|
|
498
|
-
request["sl"] = (_price + stop_loss * point)
|
|
499
|
-
request["tp"] = (_price - take_profit * point)
|
|
518
|
+
mm_price = _price
|
|
500
519
|
if action != 'SMKT':
|
|
501
520
|
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
502
521
|
request["type"] = self._order_type()[action][0]
|
|
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)
|
|
503
534
|
self.break_even(mm=mm, id=Id)
|
|
504
535
|
if self.check(comment):
|
|
505
536
|
self.request_result(_price, request, action)
|
|
506
537
|
|
|
507
|
-
def _risk_free(self):
|
|
508
|
-
max_trade = self.max_trade()
|
|
509
|
-
loss_trades = self.get_stats()[0]['loss_trades']
|
|
510
|
-
if loss_trades >= max_trade:
|
|
511
|
-
return False
|
|
512
|
-
return True
|
|
513
|
-
|
|
514
538
|
def check(self, comment):
|
|
515
539
|
"""
|
|
516
540
|
Verify if all conditions for taking a position are valide,
|
|
@@ -529,16 +553,14 @@ class Trade(RiskManagement):
|
|
|
529
553
|
self.logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
|
|
530
554
|
self._check(comment)
|
|
531
555
|
return False
|
|
532
|
-
elif not self._risk_free():
|
|
533
|
-
self.logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
|
|
534
|
-
self._check(comment)
|
|
535
|
-
return False
|
|
536
556
|
elif self.profit_target():
|
|
537
557
|
self._check(f'Profit target Reached !!! SYMBOL={self.symbol}')
|
|
538
558
|
return True
|
|
539
559
|
|
|
540
560
|
def _check(self, txt: str = ""):
|
|
541
|
-
if self.positive_profit(
|
|
561
|
+
if (self.positive_profit(id=self.expert_id)
|
|
562
|
+
or self.get_current_positions() is None
|
|
563
|
+
):
|
|
542
564
|
self.close_positions(position_type='all')
|
|
543
565
|
self.logger.info(txt)
|
|
544
566
|
time.sleep(5)
|
|
@@ -565,7 +587,7 @@ class Trade(RiskManagement):
|
|
|
565
587
|
pos = self._order_type()[type][1]
|
|
566
588
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
567
589
|
try:
|
|
568
|
-
|
|
590
|
+
check = self.check_order(request)
|
|
569
591
|
result = self.send_order(request)
|
|
570
592
|
except Exception as e:
|
|
571
593
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -581,7 +603,7 @@ class Trade(RiskManagement):
|
|
|
581
603
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
582
604
|
time.sleep(1)
|
|
583
605
|
try:
|
|
584
|
-
|
|
606
|
+
check = self.check_order(request)
|
|
585
607
|
result = self.send_order(request)
|
|
586
608
|
except Exception as e:
|
|
587
609
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -630,6 +652,7 @@ class Trade(RiskManagement):
|
|
|
630
652
|
self,
|
|
631
653
|
action: Buys | Sells,
|
|
632
654
|
price: Optional[float] = None,
|
|
655
|
+
stoplimit: Optional[float] = None,
|
|
633
656
|
id: Optional[int] = None,
|
|
634
657
|
mm: bool = True,
|
|
635
658
|
comment: Optional[str] = None
|
|
@@ -640,6 +663,9 @@ class Trade(RiskManagement):
|
|
|
640
663
|
Args:
|
|
641
664
|
action (str): (`'BMKT'`, `'SMKT'`) for Market orders
|
|
642
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
|
|
643
669
|
id (int): The strategy id or expert Id
|
|
644
670
|
mm (bool): Weither to put stop loss and tp or not
|
|
645
671
|
comment (str): The comment for the closing position
|
|
@@ -648,43 +674,43 @@ class Trade(RiskManagement):
|
|
|
648
674
|
SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
649
675
|
if action in BUYS:
|
|
650
676
|
self.open_buy_position(
|
|
651
|
-
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
677
|
+
action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
652
678
|
elif action in SELLS:
|
|
653
679
|
self.open_sell_position(
|
|
654
|
-
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
680
|
+
action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
655
681
|
else:
|
|
656
682
|
raise ValueError(f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
|
|
657
683
|
|
|
658
684
|
@property
|
|
659
|
-
def
|
|
685
|
+
def orders(self):
|
|
660
686
|
""" Return all opened order's tickets"""
|
|
661
687
|
if len(self.opened_orders) != 0:
|
|
662
688
|
return self.opened_orders
|
|
663
689
|
return None
|
|
664
690
|
|
|
665
691
|
@property
|
|
666
|
-
def
|
|
692
|
+
def positions(self):
|
|
667
693
|
"""Return all opened position's tickets"""
|
|
668
694
|
if len(self.opened_positions) != 0:
|
|
669
695
|
return self.opened_positions
|
|
670
696
|
return None
|
|
671
697
|
|
|
672
698
|
@property
|
|
673
|
-
def
|
|
699
|
+
def buypos(self):
|
|
674
700
|
"""Return all buy opened position's tickets"""
|
|
675
701
|
if len(self.buy_positions) != 0:
|
|
676
702
|
return self.buy_positions
|
|
677
703
|
return None
|
|
678
704
|
|
|
679
705
|
@property
|
|
680
|
-
def
|
|
706
|
+
def sellpos(self):
|
|
681
707
|
"""Return all sell opened position's tickets"""
|
|
682
708
|
if len(self.sell_positions) != 0:
|
|
683
709
|
return self.sell_positions
|
|
684
710
|
return None
|
|
685
711
|
|
|
686
712
|
@property
|
|
687
|
-
def
|
|
713
|
+
def bepos(self):
|
|
688
714
|
"""Return All positon's tickets
|
|
689
715
|
for which a break even has been set"""
|
|
690
716
|
if len(self.break_even_status) != 0:
|
|
@@ -701,8 +727,14 @@ class Trade(RiskManagement):
|
|
|
701
727
|
|
|
702
728
|
Args:
|
|
703
729
|
id (int): The strategy id or expert Id
|
|
704
|
-
filter_type (str): Filter type
|
|
730
|
+
filter_type (str): Filter type to apply on the tickets,
|
|
705
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
|
|
706
738
|
- `positions` are all current open positions
|
|
707
739
|
- `buys` and `sells` are current buy or sell open positions
|
|
708
740
|
- `profitables` are current open position that have a profit greater than a threshold
|
|
@@ -715,8 +747,9 @@ class Trade(RiskManagement):
|
|
|
715
747
|
or None if no tickets match the criteria.
|
|
716
748
|
"""
|
|
717
749
|
Id = id if id is not None else self.expert_id
|
|
750
|
+
POSITIONS = ['positions', 'buys', 'sells', 'profitables', 'losings']
|
|
718
751
|
|
|
719
|
-
if filter_type
|
|
752
|
+
if filter_type not in POSITIONS:
|
|
720
753
|
items = self.get_orders(symbol=self.symbol)
|
|
721
754
|
else:
|
|
722
755
|
items = self.get_positions(symbol=self.symbol)
|
|
@@ -730,18 +763,48 @@ class Trade(RiskManagement):
|
|
|
730
763
|
continue
|
|
731
764
|
if filter_type == 'sells' and item.type != 1:
|
|
732
765
|
continue
|
|
766
|
+
if filter_type == 'losings' and item.profit > 0:
|
|
767
|
+
continue
|
|
733
768
|
if filter_type == 'profitables' and not self.win_trade(item, th=th):
|
|
734
769
|
continue
|
|
735
|
-
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]:
|
|
736
781
|
continue
|
|
737
782
|
filtered_tickets.append(item.ticket)
|
|
738
783
|
return filtered_tickets if filtered_tickets else None
|
|
739
784
|
return None
|
|
740
785
|
|
|
741
|
-
def
|
|
786
|
+
def get_current_orders(self, id: Optional[int] = None) -> List[int] | None:
|
|
742
787
|
return self.get_filtered_tickets(id=id, filter_type='orders')
|
|
743
788
|
|
|
744
|
-
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:
|
|
745
808
|
return self.get_filtered_tickets(id=id, filter_type='positions')
|
|
746
809
|
|
|
747
810
|
def get_current_profitables(self, id: Optional[int] = None, th=None) -> List[int] | None:
|
|
@@ -868,10 +931,14 @@ class Trade(RiskManagement):
|
|
|
868
931
|
# This level validate the favorable move of the price
|
|
869
932
|
new_level = round(position.price_open + (new_be_points * point), digits)
|
|
870
933
|
# This price is set away from the current price by the trail_points
|
|
871
|
-
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
|
|
872
937
|
elif position.type == 1:
|
|
873
938
|
new_level = round(position.price_open - (new_be_points * point), digits)
|
|
874
|
-
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
|
|
875
942
|
self.set_break_even(
|
|
876
943
|
position, be, price=new_price, level=new_level
|
|
877
944
|
)
|
|
@@ -916,7 +983,7 @@ class Trade(RiskManagement):
|
|
|
916
983
|
"sl": round(_price, digits),
|
|
917
984
|
"tp": position.tp
|
|
918
985
|
}
|
|
919
|
-
self.
|
|
986
|
+
self.break_even_request(
|
|
920
987
|
position.ticket, round(_price, digits), request)
|
|
921
988
|
# If Sell
|
|
922
989
|
elif position.type == 1 and position.price_current < position.price_open:
|
|
@@ -936,10 +1003,10 @@ class Trade(RiskManagement):
|
|
|
936
1003
|
"sl": round(_price, digits),
|
|
937
1004
|
"tp": position.tp
|
|
938
1005
|
}
|
|
939
|
-
self.
|
|
1006
|
+
self.break_even_request(
|
|
940
1007
|
position.ticket, round(_price, digits), request)
|
|
941
1008
|
|
|
942
|
-
def
|
|
1009
|
+
def break_even_request(self, tiket, price, request):
|
|
943
1010
|
"""
|
|
944
1011
|
Send a request to set the stop loss to break even for a given trading position.
|
|
945
1012
|
|
|
@@ -994,6 +1061,7 @@ class Trade(RiskManagement):
|
|
|
994
1061
|
wen it is closed before be level , tp or sl.
|
|
995
1062
|
|
|
996
1063
|
Args:
|
|
1064
|
+
position (TradePosition): The trading position to check.
|
|
997
1065
|
th (int): The minimum profit for a position in point
|
|
998
1066
|
"""
|
|
999
1067
|
size = self.get_symbol_info(self.symbol).trade_tick_size
|
|
@@ -1044,6 +1112,75 @@ class Trade(RiskManagement):
|
|
|
1044
1112
|
return True
|
|
1045
1113
|
return False
|
|
1046
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
|
+
|
|
1047
1184
|
def close_position(self,
|
|
1048
1185
|
ticket: int,
|
|
1049
1186
|
id: Optional[int] = None,
|
|
@@ -1090,41 +1227,77 @@ class Trade(RiskManagement):
|
|
|
1090
1227
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
1091
1228
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
1092
1229
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
if
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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)
|
|
1128
1301
|
|
|
1129
1302
|
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
1130
1303
|
def close_positions(
|
|
@@ -1138,8 +1311,9 @@ class Trade(RiskManagement):
|
|
|
1138
1311
|
id (int): The unique ID of the Expert or Strategy
|
|
1139
1312
|
comment (str): Comment for the closing position
|
|
1140
1313
|
"""
|
|
1314
|
+
id = id if id is not None else self.expert_id
|
|
1141
1315
|
if position_type == "all":
|
|
1142
|
-
positions = self.
|
|
1316
|
+
positions = self.get_current_positions(id=id)
|
|
1143
1317
|
elif position_type == "buy":
|
|
1144
1318
|
positions = self.get_current_buys(id=id)
|
|
1145
1319
|
elif position_type == "sell":
|
|
@@ -1151,35 +1325,8 @@ class Trade(RiskManagement):
|
|
|
1151
1325
|
else:
|
|
1152
1326
|
self.logger.error(f"Invalid position type: {position_type}")
|
|
1153
1327
|
return
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
if position_type == 'all':
|
|
1157
|
-
tickets = [position.ticket for position in positions if position.magic == id]
|
|
1158
|
-
else:
|
|
1159
|
-
tickets = positions
|
|
1160
|
-
else:
|
|
1161
|
-
tickets = []
|
|
1162
|
-
|
|
1163
|
-
if position_type == 'all':
|
|
1164
|
-
pos_type = 'open'
|
|
1165
|
-
else:
|
|
1166
|
-
pos_type = position_type
|
|
1167
|
-
|
|
1168
|
-
if len(tickets) != 0:
|
|
1169
|
-
for ticket in tickets.copy():
|
|
1170
|
-
if self.close_position(ticket, id=id, comment=comment):
|
|
1171
|
-
tickets.remove(ticket)
|
|
1172
|
-
time.sleep(1)
|
|
1173
|
-
|
|
1174
|
-
if len(tickets) == 0:
|
|
1175
|
-
self.logger.info(
|
|
1176
|
-
f"ALL {pos_type.upper()} Positions closed, SYMBOL={self.symbol}.")
|
|
1177
|
-
else:
|
|
1178
|
-
self.logger.info(
|
|
1179
|
-
f"{len(tickets)} {pos_type.upper()} Positions not closed, SYMBOL={self.symbol}")
|
|
1180
|
-
else:
|
|
1181
|
-
self.logger.info(
|
|
1182
|
-
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)
|
|
1183
1330
|
|
|
1184
1331
|
def get_stats(self) -> Tuple[Dict[str, Any]]:
|
|
1185
1332
|
"""
|
|
@@ -1339,7 +1486,7 @@ def create_trade_instance(
|
|
|
1339
1486
|
daily_risk: Optional[Dict[str, float]] = None,
|
|
1340
1487
|
max_risk: Optional[Dict[str, float]] = None,
|
|
1341
1488
|
pchange_sl: Optional[Dict[str, float] | float] = None,
|
|
1342
|
-
|
|
1489
|
+
) -> Dict[str, Trade]:
|
|
1343
1490
|
"""
|
|
1344
1491
|
Creates Trade instances for each symbol provided.
|
|
1345
1492
|
|
|
@@ -1348,7 +1495,6 @@ def create_trade_instance(
|
|
|
1348
1495
|
params: A dictionary containing parameters for the Trade instance.
|
|
1349
1496
|
daily_risk: A dictionary containing daily risk weight for each symbol.
|
|
1350
1497
|
max_risk: A dictionary containing maximum risk weight for each symbol.
|
|
1351
|
-
logger: A logger instance.
|
|
1352
1498
|
|
|
1353
1499
|
Returns:
|
|
1354
1500
|
A dictionary where keys are symbols and values are corresponding Trade instances.
|
|
@@ -1360,7 +1506,8 @@ def create_trade_instance(
|
|
|
1360
1506
|
`daily_risk` and `max_risk` can be used to manage the risk of each symbol
|
|
1361
1507
|
based on the importance of the symbol in the portfolio or strategy.
|
|
1362
1508
|
"""
|
|
1363
|
-
|
|
1509
|
+
logger = params.get('logger', None)
|
|
1510
|
+
trade_instances = {}
|
|
1364
1511
|
if not symbols:
|
|
1365
1512
|
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1366
1513
|
if not params:
|
|
@@ -1385,19 +1532,28 @@ def create_trade_instance(
|
|
|
1385
1532
|
params['symbol'] = symbol
|
|
1386
1533
|
params['pchange_sl'] = (
|
|
1387
1534
|
pchange_sl[symbol] if pchange_sl is not None
|
|
1388
|
-
and isinstance(pchange_sl, dict) else
|
|
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
|
|
1389
1547
|
)
|
|
1390
|
-
|
|
1391
|
-
params['max_risk'] = max_risk[symbol] if max_risk is not None else params['max_risk']
|
|
1392
|
-
instances[symbol] = Trade(**params)
|
|
1548
|
+
trade_instances[symbol] = Trade(**params)
|
|
1393
1549
|
except Exception as e:
|
|
1394
1550
|
logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1395
1551
|
|
|
1396
|
-
if len(
|
|
1552
|
+
if len(trade_instances) != len(symbols):
|
|
1397
1553
|
for symbol in symbols:
|
|
1398
|
-
if symbol not in
|
|
1399
|
-
if logger is not None:
|
|
1554
|
+
if symbol not in trade_instances:
|
|
1555
|
+
if logger is not None and isinstance(logger, Logger):
|
|
1400
1556
|
logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1401
1557
|
else:
|
|
1402
1558
|
raise ValueError(f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1403
|
-
return
|
|
1559
|
+
return trade_instances
|