bbstrader 0.2.92__py3-none-any.whl → 0.2.94__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 +20 -20
- bbstrader/__main__.py +50 -50
- bbstrader/btengine/__init__.py +54 -54
- bbstrader/btengine/data.py +11 -9
- bbstrader/btengine/scripts.py +157 -157
- bbstrader/compat.py +19 -19
- bbstrader/config.py +137 -137
- bbstrader/core/data.py +22 -22
- bbstrader/core/utils.py +146 -146
- bbstrader/metatrader/__init__.py +6 -6
- bbstrader/metatrader/account.py +1516 -1516
- bbstrader/metatrader/copier.py +750 -735
- bbstrader/metatrader/rates.py +584 -584
- bbstrader/metatrader/risk.py +749 -748
- bbstrader/metatrader/scripts.py +81 -81
- bbstrader/metatrader/trade.py +1836 -1826
- bbstrader/metatrader/utils.py +645 -645
- bbstrader/models/__init__.py +10 -10
- bbstrader/models/factors.py +312 -312
- bbstrader/models/ml.py +1272 -1265
- bbstrader/models/optimization.py +182 -182
- bbstrader/models/portfolio.py +223 -223
- bbstrader/models/risk.py +398 -398
- bbstrader/trading/__init__.py +11 -11
- bbstrader/trading/execution.py +846 -842
- bbstrader/trading/script.py +155 -155
- bbstrader/trading/scripts.py +69 -69
- bbstrader/trading/strategies.py +860 -860
- bbstrader/tseries.py +1842 -1842
- {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/LICENSE +21 -21
- {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/METADATA +188 -187
- bbstrader-0.2.94.dist-info/RECORD +44 -0
- {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/WHEEL +1 -1
- bbstrader-0.2.92.dist-info/RECORD +0 -44
- {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -1,1826 +1,1836 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import time
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from logging import Logger
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
|
|
7
|
-
|
|
8
|
-
import pandas as pd
|
|
9
|
-
from loguru import logger as log
|
|
10
|
-
from tabulate import tabulate
|
|
11
|
-
|
|
12
|
-
from bbstrader.btengine.performance import create_sharpe_ratio
|
|
13
|
-
from bbstrader.config import BBSTRADER_DIR, config_logger
|
|
14
|
-
from bbstrader.metatrader.account import INIT_MSG, check_mt5_connection
|
|
15
|
-
from bbstrader.metatrader.risk import RiskManagement
|
|
16
|
-
from bbstrader.metatrader.utils import (
|
|
17
|
-
TradePosition,
|
|
18
|
-
raise_mt5_error,
|
|
19
|
-
trade_retcode_message,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
import MetaTrader5 as Mt5
|
|
24
|
-
except ImportError:
|
|
25
|
-
import bbstrader.compat # noqa: F401
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
__all__ = [
|
|
29
|
-
"Trade",
|
|
30
|
-
"create_trade_instance",
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
FILLING_TYPE = [
|
|
34
|
-
Mt5.ORDER_FILLING_IOC,
|
|
35
|
-
Mt5.ORDER_FILLING_RETURN,
|
|
36
|
-
Mt5.ORDER_FILLING_BOC,
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
log.add(
|
|
40
|
-
f"{BBSTRADER_DIR}/logs/trade.log",
|
|
41
|
-
enqueue=True,
|
|
42
|
-
level="INFO",
|
|
43
|
-
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
global LOGGER
|
|
47
|
-
LOGGER = log
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class Trade(RiskManagement):
|
|
51
|
-
"""
|
|
52
|
-
Extends the `RiskManagement` class to include specific trading operations,
|
|
53
|
-
incorporating risk management strategies directly into trade executions.
|
|
54
|
-
It offers functionalities to execute trades while managing risks
|
|
55
|
-
according to the inherited RiskManagement parameters and methods.
|
|
56
|
-
|
|
57
|
-
Exemple:
|
|
58
|
-
>>> import time
|
|
59
|
-
>>> # Initialize the Trade class with parameters
|
|
60
|
-
>>> trade = Trade(
|
|
61
|
-
... symbol="EURUSD", # Symbol to trade
|
|
62
|
-
... expert_name="bbstrader", # Name of the expert advisor
|
|
63
|
-
... expert_id=12345, # Unique ID for the expert advisor
|
|
64
|
-
... version="1.0", # Version of the expert advisor
|
|
65
|
-
... target=5.0, # Daily profit target in percentage
|
|
66
|
-
... start_time="09:00", # Start time for trading
|
|
67
|
-
... finishing_time="17:00", # Time to stop opening new positions
|
|
68
|
-
... ending_time="17:30", # Time to close any open positions
|
|
69
|
-
... max_risk=2.0, # Maximum risk allowed on the account in percentage
|
|
70
|
-
... daily_risk=1.0, # Daily risk allowed in percentage
|
|
71
|
-
... max_trades=5, # Maximum number of trades per session
|
|
72
|
-
... rr=2.0, # Risk-reward ratio
|
|
73
|
-
... account_leverage=True, # Use account leverage in calculations
|
|
74
|
-
... std_stop=True, # Use standard deviation for stop loss calculation
|
|
75
|
-
... sl=20, # Stop loss in points (optional)
|
|
76
|
-
... tp=30, # Take profit in points (optional)
|
|
77
|
-
... be=10 # Break-even in points (optional)
|
|
78
|
-
... )
|
|
79
|
-
|
|
80
|
-
>>> # Example to open a buy position
|
|
81
|
-
>>> trade.open_buy_position(mm=True, comment="Opening Buy Position")
|
|
82
|
-
|
|
83
|
-
>>> # Example to open a sell position
|
|
84
|
-
>>> trade.open_sell_position(mm=True, comment="Opening Sell Position")
|
|
85
|
-
|
|
86
|
-
>>> # Check current open positions
|
|
87
|
-
>>> opened_positions = trade.get_opened_positions
|
|
88
|
-
>>> if opened_positions is not None:
|
|
89
|
-
... print(f"Current open positions: {opened_positions}")
|
|
90
|
-
|
|
91
|
-
>>> # Close all open positions at the end of the trading session
|
|
92
|
-
>>> if trade.days_end():
|
|
93
|
-
... trade.close_all_positions(comment="Closing all positions at day's end")
|
|
94
|
-
|
|
95
|
-
>>> # Print trading session statistics
|
|
96
|
-
>>> trade.statistics(save=True, dir="my_trading_stats")
|
|
97
|
-
|
|
98
|
-
>>> # Sleep until the next trading session if needed (example usage)
|
|
99
|
-
>>> sleep_time = trade.sleep_time()
|
|
100
|
-
>>> print(f"Sleeping for {sleep_time} minutes until the next trading session.")
|
|
101
|
-
>>> time.sleep(sleep_time * 60)
|
|
102
|
-
"""
|
|
103
|
-
|
|
104
|
-
def __init__(
|
|
105
|
-
self,
|
|
106
|
-
symbol: str = "EURUSD",
|
|
107
|
-
expert_name: str = "bbstrader",
|
|
108
|
-
expert_id: int = 9818,
|
|
109
|
-
version: str = "2.0",
|
|
110
|
-
target: float = 5.0,
|
|
111
|
-
start_time: str = "0:00",
|
|
112
|
-
finishing_time: str = "23:59",
|
|
113
|
-
ending_time: str = "23:59",
|
|
114
|
-
verbose: Optional[bool] = None,
|
|
115
|
-
console_log: Optional[bool] = False,
|
|
116
|
-
logger: Logger | str = "bbstrader.log",
|
|
117
|
-
**kwargs,
|
|
118
|
-
):
|
|
119
|
-
"""
|
|
120
|
-
Initializes the Trade class with the specified parameters.
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
symbol (str): The `symbol` that the expert advisor will trade.
|
|
124
|
-
expert_name (str): The name of the `expert advisor`.
|
|
125
|
-
expert_id (int): The `unique ID` used to identify the expert advisor
|
|
126
|
-
or the strategy used on the symbol.
|
|
127
|
-
version (str): The `version` of the expert advisor.
|
|
128
|
-
target (float): `Trading period (day, week, month) profit target` in percentage.
|
|
129
|
-
start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
|
|
130
|
-
finishing_time (str): The time after which no new position can be opened.
|
|
131
|
-
ending_time (str): The time after which any open position will be closed.
|
|
132
|
-
verbose (bool | None): If set to None (default), account summary and risk managment
|
|
133
|
-
parameters are printed in the terminal.
|
|
134
|
-
console_log (bool): If set to True, log messages are displayed in the console.
|
|
135
|
-
logger (Logger | str): The logger object to use for logging messages could be a string or a logger object.
|
|
136
|
-
|
|
137
|
-
Inherits:
|
|
138
|
-
- max_risk
|
|
139
|
-
- max_trades
|
|
140
|
-
- rr
|
|
141
|
-
- daily_risk
|
|
142
|
-
- time_frame
|
|
143
|
-
- account_leverage
|
|
144
|
-
- std_stop
|
|
145
|
-
- pchange_sl
|
|
146
|
-
- sl
|
|
147
|
-
- tp
|
|
148
|
-
- be
|
|
149
|
-
See the ``bbstrader.metatrader.risk.RiskManagement`` class for more details on these parameters.
|
|
150
|
-
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
151
|
-
"""
|
|
152
|
-
# Call the parent class constructor first
|
|
153
|
-
super().__init__(
|
|
154
|
-
symbol=symbol,
|
|
155
|
-
start_time=start_time,
|
|
156
|
-
finishing_time=finishing_time,
|
|
157
|
-
**kwargs, # Pass kwargs to the parent constructor
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
# Initialize Trade-specific attributes
|
|
161
|
-
self.symbol = symbol
|
|
162
|
-
self.expert_name = expert_name
|
|
163
|
-
self.expert_id = expert_id
|
|
164
|
-
self.version = version
|
|
165
|
-
self.target = target
|
|
166
|
-
self.verbose = verbose
|
|
167
|
-
self.start = start_time
|
|
168
|
-
self.end = ending_time
|
|
169
|
-
self.finishing = finishing_time
|
|
170
|
-
self.console_log = console_log
|
|
171
|
-
self.tf = kwargs.get("time_frame", "D1")
|
|
172
|
-
self.kwargs = kwargs
|
|
173
|
-
|
|
174
|
-
self.start_time_hour, self.start_time_minutes = self.start.split(":")
|
|
175
|
-
self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
|
|
176
|
-
":"
|
|
177
|
-
)
|
|
178
|
-
self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
|
|
179
|
-
|
|
180
|
-
self.buy_positions = []
|
|
181
|
-
self.sell_positions = []
|
|
182
|
-
self.opened_positions = []
|
|
183
|
-
self.opened_orders = []
|
|
184
|
-
self.break_even_status = []
|
|
185
|
-
self.break_even_points = {}
|
|
186
|
-
self.trail_after_points = []
|
|
187
|
-
self._retcodes = []
|
|
188
|
-
|
|
189
|
-
self._get_logger(logger, console_log)
|
|
190
|
-
self.initialize(**kwargs)
|
|
191
|
-
self.select_symbol(**kwargs)
|
|
192
|
-
self.prepare_symbol()
|
|
193
|
-
|
|
194
|
-
if self.verbose:
|
|
195
|
-
self.summary()
|
|
196
|
-
time.sleep(1)
|
|
197
|
-
print()
|
|
198
|
-
self.risk_managment()
|
|
199
|
-
print(f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
|
|
200
|
-
|
|
201
|
-
@property
|
|
202
|
-
def retcodes(self) -> List[int]:
|
|
203
|
-
"""Return all the retcodes"""
|
|
204
|
-
return self._retcodes
|
|
205
|
-
|
|
206
|
-
@property
|
|
207
|
-
def logger(self):
|
|
208
|
-
return LOGGER
|
|
209
|
-
|
|
210
|
-
def _get_logger(self, loger: Any, consol_log: bool):
|
|
211
|
-
"""Get the logger object"""
|
|
212
|
-
global LOGGER
|
|
213
|
-
if loger is None:
|
|
214
|
-
... # Do nothing
|
|
215
|
-
elif isinstance(loger, (str, Path)):
|
|
216
|
-
LOGGER = config_logger(f"{BBSTRADER_DIR}/logs/{loger}", consol_log)
|
|
217
|
-
elif isinstance(loger, (Logger, type(log))):
|
|
218
|
-
LOGGER = loger
|
|
219
|
-
|
|
220
|
-
def initialize(self, **kwargs):
|
|
221
|
-
"""
|
|
222
|
-
Initializes the MetaTrader 5 (MT5) terminal for trading operations.
|
|
223
|
-
This method attempts to establish a connection with the MT5 terminal.
|
|
224
|
-
If the initial connection attempt fails due to a timeout, it retries after a specified delay.
|
|
225
|
-
Successful initialization is crucial for the execution of trading operations.
|
|
226
|
-
|
|
227
|
-
Raises:
|
|
228
|
-
MT5TerminalError: If initialization fails.
|
|
229
|
-
"""
|
|
230
|
-
try:
|
|
231
|
-
if self.verbose:
|
|
232
|
-
print("\nInitializing the basics.")
|
|
233
|
-
check_mt5_connection(**kwargs)
|
|
234
|
-
if self.verbose:
|
|
235
|
-
print(
|
|
236
|
-
f"You are running the @{self.expert_name} Expert advisor,"
|
|
237
|
-
f" Version @{self.version}, on {self.symbol}."
|
|
238
|
-
)
|
|
239
|
-
except Exception as e:
|
|
240
|
-
LOGGER.error(f"During initialization: {e}")
|
|
241
|
-
|
|
242
|
-
def select_symbol(self, **kwargs):
|
|
243
|
-
"""
|
|
244
|
-
Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
|
|
245
|
-
This method ensures that the specified trading
|
|
246
|
-
symbol is selected and visible in the MT5 terminal,
|
|
247
|
-
allowing subsequent trading operations such as opening and
|
|
248
|
-
closing positions on this symbol.
|
|
249
|
-
|
|
250
|
-
Raises:
|
|
251
|
-
MT5TerminalError: If symbole selection fails.
|
|
252
|
-
"""
|
|
253
|
-
try:
|
|
254
|
-
check_mt5_connection(**kwargs)
|
|
255
|
-
if not Mt5.symbol_select(self.symbol, True):
|
|
256
|
-
raise_mt5_error(message=INIT_MSG)
|
|
257
|
-
except Exception as e:
|
|
258
|
-
LOGGER.error(f"Selecting symbol '{self.symbol}': {e}")
|
|
259
|
-
|
|
260
|
-
def prepare_symbol(self):
|
|
261
|
-
"""
|
|
262
|
-
Prepares the selected symbol for trading.
|
|
263
|
-
This method checks if the symbol is available and visible in the
|
|
264
|
-
MT5 terminal. If the symbol is not visible, it attempts to select the symbol again.
|
|
265
|
-
This step ensures that trading operations can be performed on the selected symbol without issues.
|
|
266
|
-
|
|
267
|
-
Raises:
|
|
268
|
-
MT5TerminalError: If the symbol cannot be made visible for trading operations.
|
|
269
|
-
"""
|
|
270
|
-
try:
|
|
271
|
-
symbol_info = self.get_symbol_info(self.symbol)
|
|
272
|
-
if symbol_info is None:
|
|
273
|
-
raise_mt5_error(message=INIT_MSG)
|
|
274
|
-
|
|
275
|
-
if not symbol_info.visible:
|
|
276
|
-
raise_mt5_error(message=INIT_MSG)
|
|
277
|
-
if self.verbose:
|
|
278
|
-
print("Initialization successfully completed.")
|
|
279
|
-
except Exception as e:
|
|
280
|
-
LOGGER.error(f"Preparing symbol '{self.symbol}': {e}")
|
|
281
|
-
|
|
282
|
-
def summary(self):
|
|
283
|
-
"""Show a brief description about the trading program"""
|
|
284
|
-
summary_data = [
|
|
285
|
-
["Expert Advisor Name", f"@{self.expert_name}"],
|
|
286
|
-
["Expert Advisor Version", f"@{self.version}"],
|
|
287
|
-
["Expert | Strategy ID", self.expert_id],
|
|
288
|
-
["Trading Symbol", self.symbol],
|
|
289
|
-
["Trading Time Frame", self.tf],
|
|
290
|
-
["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
|
|
291
|
-
[
|
|
292
|
-
"Finishing Trading Time",
|
|
293
|
-
f"{self.finishing_time_hour}:{self.finishing_time_minutes}",
|
|
294
|
-
],
|
|
295
|
-
[
|
|
296
|
-
"Closing Position After",
|
|
297
|
-
f"{self.ending_time_hour}:{self.ending_time_minutes}",
|
|
298
|
-
],
|
|
299
|
-
]
|
|
300
|
-
# Custom table format
|
|
301
|
-
summary_table = tabulate(
|
|
302
|
-
summary_data, headers=["Summary", "Values"], tablefmt="outline"
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
# Print the table
|
|
306
|
-
print("\n[======= Trade Account Summary =======]")
|
|
307
|
-
print(summary_table)
|
|
308
|
-
|
|
309
|
-
def risk_managment(self):
|
|
310
|
-
"""Show the risk management parameters"""
|
|
311
|
-
|
|
312
|
-
loss = self.currency_risk()["trade_loss"]
|
|
313
|
-
profit = self.currency_risk()["trade_profit"]
|
|
314
|
-
ok = "OK" if self.is_risk_ok() else "Not OK"
|
|
315
|
-
account_info = self.get_account_info()
|
|
316
|
-
_profit = round(self.get_stats()[1]["total_profit"], 2)
|
|
317
|
-
currency = account_info.currency
|
|
318
|
-
rates = self.get_currency_rates(self.symbol)
|
|
319
|
-
marging_currency = rates["mc"]
|
|
320
|
-
account_data = [
|
|
321
|
-
["Account Name", account_info.name],
|
|
322
|
-
["Account Number", account_info.login],
|
|
323
|
-
["Account Server", account_info.server],
|
|
324
|
-
["Account Balance", f"{account_info.balance} {currency}"],
|
|
325
|
-
["Account Profit", f"{_profit} {currency}"],
|
|
326
|
-
["Account Equity", f"{account_info.equity} {currency}"],
|
|
327
|
-
["Account Leverage", self.get_leverage(True)],
|
|
328
|
-
["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
|
|
329
|
-
["Account Free Margin", f"{account_info.margin_free} {currency}"],
|
|
330
|
-
["Maximum Drawdown", f"{self.max_risk}%"],
|
|
331
|
-
["Risk Allowed", f"{round((self.max_risk - self.risk_level()), 2)}%"],
|
|
332
|
-
["Volume", f"{self.volume()} {marging_currency}"],
|
|
333
|
-
["Risk Per trade", f"{-self.get_currency_risk()} {currency}"],
|
|
334
|
-
["Profit Expected Per trade", f"{self.expected_profit()} {currency}"],
|
|
335
|
-
["Lot Size", f"{self.get_lot()} Lots"],
|
|
336
|
-
["Stop Loss", f"{self.get_stop_loss()} Points"],
|
|
337
|
-
["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
|
|
338
|
-
["Take Profit", f"{self.get_take_profit()} Points"],
|
|
339
|
-
["Profit Value Per Tick", f"{round(profit, 5)} {currency}"],
|
|
340
|
-
["Break Even", f"{self.get_break_even()} Points"],
|
|
341
|
-
["Deviation", f"{self.get_deviation()} Points"],
|
|
342
|
-
["Trading Time Interval", f"{self.get_minutes()} Minutes"],
|
|
343
|
-
["Risk Level", ok],
|
|
344
|
-
["Maximum Trades", self.max_trade()],
|
|
345
|
-
]
|
|
346
|
-
# Custom table format
|
|
347
|
-
print("\n[======= Account Risk Management Overview =======]")
|
|
348
|
-
table = tabulate(
|
|
349
|
-
account_data, headers=["Risk Metrics", "Values"], tablefmt="outline"
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
# Print the table
|
|
353
|
-
print(table)
|
|
354
|
-
|
|
355
|
-
def statistics(self, save=True, dir=None):
|
|
356
|
-
"""
|
|
357
|
-
Print some statistics for the trading session and save to CSV if specified.
|
|
358
|
-
|
|
359
|
-
Args:
|
|
360
|
-
save (bool, optional): Whether to save the statistics to a CSV file.
|
|
361
|
-
dir (str, optional): The directory to save the CSV file.
|
|
362
|
-
"""
|
|
363
|
-
stats, additional_stats = self.get_stats()
|
|
364
|
-
|
|
365
|
-
profit = round(stats["profit"], 2)
|
|
366
|
-
win_rate = stats["win_rate"]
|
|
367
|
-
total_fees = round(stats["total_fees"], 3)
|
|
368
|
-
average_fee = round(stats["average_fee"], 3)
|
|
369
|
-
currency = self.get_account_info().currency
|
|
370
|
-
net_profit = round((profit + total_fees), 2)
|
|
371
|
-
trade_risk = round(self.get_currency_risk() * -1, 2)
|
|
372
|
-
|
|
373
|
-
# Formatting the statistics output
|
|
374
|
-
session_data = [
|
|
375
|
-
["Total Trades", stats["deals"]],
|
|
376
|
-
["Winning Trades", stats["win_trades"]],
|
|
377
|
-
["Losing Trades", stats["loss_trades"]],
|
|
378
|
-
["Session Profit", f"{profit} {currency}"],
|
|
379
|
-
["Total Fees", f"{total_fees} {currency}"],
|
|
380
|
-
["Average Fees", f"{average_fee} {currency}"],
|
|
381
|
-
["Net Profit", f"{net_profit} {currency}"],
|
|
382
|
-
["Risk per Trade", f"{trade_risk} {currency}"],
|
|
383
|
-
["Expected Profit per Trade", f"{self.expected_profit()} {currency}"],
|
|
384
|
-
["Risk Reward Ratio", self.rr],
|
|
385
|
-
["Win Rate", f"{win_rate}%"],
|
|
386
|
-
["Sharpe Ratio", self.sharpe()],
|
|
387
|
-
["Trade Profitability", additional_stats["profitability"]],
|
|
388
|
-
]
|
|
389
|
-
session_table = tabulate(
|
|
390
|
-
session_data, headers=["Statistics", "Values"], tablefmt="outline"
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
# Print the formatted statistics
|
|
394
|
-
if self.verbose:
|
|
395
|
-
print("\n[======= Trading Session Statistics =======]")
|
|
396
|
-
print(session_table)
|
|
397
|
-
|
|
398
|
-
# Save to CSV if specified
|
|
399
|
-
if save:
|
|
400
|
-
today_date = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
401
|
-
# Create a dictionary with the statistics
|
|
402
|
-
statistics_dict = {item[0]: item[1] for item in session_data}
|
|
403
|
-
stats_df = pd.DataFrame(statistics_dict, index=[0])
|
|
404
|
-
# Create the directory if it doesn't exist
|
|
405
|
-
dir = dir or ".sessions"
|
|
406
|
-
os.makedirs(dir, exist_ok=True)
|
|
407
|
-
if "." in self.symbol:
|
|
408
|
-
symbol = self.symbol.split(".")[0]
|
|
409
|
-
else:
|
|
410
|
-
symbol = self.symbol
|
|
411
|
-
|
|
412
|
-
filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
|
|
413
|
-
filepath = os.path.join(dir, filename)
|
|
414
|
-
stats_df.to_csv(filepath, index=False)
|
|
415
|
-
LOGGER.info(f"Session statistics saved to {filepath}")
|
|
416
|
-
|
|
417
|
-
Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
418
|
-
|
|
419
|
-
def open_buy_position(
|
|
420
|
-
self,
|
|
421
|
-
action: Buys = "BMKT",
|
|
422
|
-
price: Optional[float] = None,
|
|
423
|
-
stoplimit: Optional[float] = None,
|
|
424
|
-
mm: bool = True,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
"
|
|
463
|
-
"
|
|
464
|
-
"
|
|
465
|
-
"
|
|
466
|
-
"
|
|
467
|
-
"
|
|
468
|
-
"
|
|
469
|
-
"
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if stoplimit
|
|
479
|
-
raise ValueError(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
"
|
|
497
|
-
"
|
|
498
|
-
"
|
|
499
|
-
"
|
|
500
|
-
"
|
|
501
|
-
"
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
"
|
|
557
|
-
"
|
|
558
|
-
"
|
|
559
|
-
"
|
|
560
|
-
"
|
|
561
|
-
"
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
)
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
request["
|
|
579
|
-
|
|
580
|
-
if
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
self.
|
|
601
|
-
return False
|
|
602
|
-
elif self.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
f"
|
|
708
|
-
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
- `
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
):
|
|
873
|
-
continue
|
|
874
|
-
if (
|
|
875
|
-
filter_type == "
|
|
876
|
-
and item.type != self._order_type()["
|
|
877
|
-
):
|
|
878
|
-
continue
|
|
879
|
-
if (
|
|
880
|
-
filter_type == "
|
|
881
|
-
and item.type != self._order_type()["
|
|
882
|
-
):
|
|
883
|
-
continue
|
|
884
|
-
if (
|
|
885
|
-
filter_type == "
|
|
886
|
-
and item.type != self._order_type()["
|
|
887
|
-
):
|
|
888
|
-
continue
|
|
889
|
-
if (
|
|
890
|
-
filter_type == "
|
|
891
|
-
and item.type != self._order_type()["
|
|
892
|
-
):
|
|
893
|
-
continue
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
if
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
)
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
"""
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
)
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
"""
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
""
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
if
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
"
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
"""
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
)
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
""
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
orders
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
orders = self.
|
|
1529
|
-
elif order_type == "
|
|
1530
|
-
orders = self.
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
positions
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
stats1 = {
|
|
1616
|
-
"deals":
|
|
1617
|
-
"profit":
|
|
1618
|
-
"win_trades":
|
|
1619
|
-
"loss_trades":
|
|
1620
|
-
"total_fees":
|
|
1621
|
-
"average_fee":
|
|
1622
|
-
"win_rate":
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
)
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
if
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
params["
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from loguru import logger as log
|
|
10
|
+
from tabulate import tabulate
|
|
11
|
+
|
|
12
|
+
from bbstrader.btengine.performance import create_sharpe_ratio
|
|
13
|
+
from bbstrader.config import BBSTRADER_DIR, config_logger
|
|
14
|
+
from bbstrader.metatrader.account import INIT_MSG, check_mt5_connection
|
|
15
|
+
from bbstrader.metatrader.risk import RiskManagement
|
|
16
|
+
from bbstrader.metatrader.utils import (
|
|
17
|
+
TradePosition,
|
|
18
|
+
raise_mt5_error,
|
|
19
|
+
trade_retcode_message,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import MetaTrader5 as Mt5
|
|
24
|
+
except ImportError:
|
|
25
|
+
import bbstrader.compat # noqa: F401
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Trade",
|
|
30
|
+
"create_trade_instance",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
FILLING_TYPE = [
|
|
34
|
+
Mt5.ORDER_FILLING_IOC,
|
|
35
|
+
Mt5.ORDER_FILLING_RETURN,
|
|
36
|
+
Mt5.ORDER_FILLING_BOC,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
log.add(
|
|
40
|
+
f"{BBSTRADER_DIR}/logs/trade.log",
|
|
41
|
+
enqueue=True,
|
|
42
|
+
level="INFO",
|
|
43
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
global LOGGER
|
|
47
|
+
LOGGER = log
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Trade(RiskManagement):
|
|
51
|
+
"""
|
|
52
|
+
Extends the `RiskManagement` class to include specific trading operations,
|
|
53
|
+
incorporating risk management strategies directly into trade executions.
|
|
54
|
+
It offers functionalities to execute trades while managing risks
|
|
55
|
+
according to the inherited RiskManagement parameters and methods.
|
|
56
|
+
|
|
57
|
+
Exemple:
|
|
58
|
+
>>> import time
|
|
59
|
+
>>> # Initialize the Trade class with parameters
|
|
60
|
+
>>> trade = Trade(
|
|
61
|
+
... symbol="EURUSD", # Symbol to trade
|
|
62
|
+
... expert_name="bbstrader", # Name of the expert advisor
|
|
63
|
+
... expert_id=12345, # Unique ID for the expert advisor
|
|
64
|
+
... version="1.0", # Version of the expert advisor
|
|
65
|
+
... target=5.0, # Daily profit target in percentage
|
|
66
|
+
... start_time="09:00", # Start time for trading
|
|
67
|
+
... finishing_time="17:00", # Time to stop opening new positions
|
|
68
|
+
... ending_time="17:30", # Time to close any open positions
|
|
69
|
+
... max_risk=2.0, # Maximum risk allowed on the account in percentage
|
|
70
|
+
... daily_risk=1.0, # Daily risk allowed in percentage
|
|
71
|
+
... max_trades=5, # Maximum number of trades per session
|
|
72
|
+
... rr=2.0, # Risk-reward ratio
|
|
73
|
+
... account_leverage=True, # Use account leverage in calculations
|
|
74
|
+
... std_stop=True, # Use standard deviation for stop loss calculation
|
|
75
|
+
... sl=20, # Stop loss in points (optional)
|
|
76
|
+
... tp=30, # Take profit in points (optional)
|
|
77
|
+
... be=10 # Break-even in points (optional)
|
|
78
|
+
... )
|
|
79
|
+
|
|
80
|
+
>>> # Example to open a buy position
|
|
81
|
+
>>> trade.open_buy_position(mm=True, comment="Opening Buy Position")
|
|
82
|
+
|
|
83
|
+
>>> # Example to open a sell position
|
|
84
|
+
>>> trade.open_sell_position(mm=True, comment="Opening Sell Position")
|
|
85
|
+
|
|
86
|
+
>>> # Check current open positions
|
|
87
|
+
>>> opened_positions = trade.get_opened_positions
|
|
88
|
+
>>> if opened_positions is not None:
|
|
89
|
+
... print(f"Current open positions: {opened_positions}")
|
|
90
|
+
|
|
91
|
+
>>> # Close all open positions at the end of the trading session
|
|
92
|
+
>>> if trade.days_end():
|
|
93
|
+
... trade.close_all_positions(comment="Closing all positions at day's end")
|
|
94
|
+
|
|
95
|
+
>>> # Print trading session statistics
|
|
96
|
+
>>> trade.statistics(save=True, dir="my_trading_stats")
|
|
97
|
+
|
|
98
|
+
>>> # Sleep until the next trading session if needed (example usage)
|
|
99
|
+
>>> sleep_time = trade.sleep_time()
|
|
100
|
+
>>> print(f"Sleeping for {sleep_time} minutes until the next trading session.")
|
|
101
|
+
>>> time.sleep(sleep_time * 60)
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
symbol: str = "EURUSD",
|
|
107
|
+
expert_name: str = "bbstrader",
|
|
108
|
+
expert_id: int = 9818,
|
|
109
|
+
version: str = "2.0",
|
|
110
|
+
target: float = 5.0,
|
|
111
|
+
start_time: str = "0:00",
|
|
112
|
+
finishing_time: str = "23:59",
|
|
113
|
+
ending_time: str = "23:59",
|
|
114
|
+
verbose: Optional[bool] = None,
|
|
115
|
+
console_log: Optional[bool] = False,
|
|
116
|
+
logger: Logger | str = "bbstrader.log",
|
|
117
|
+
**kwargs,
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Initializes the Trade class with the specified parameters.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
symbol (str): The `symbol` that the expert advisor will trade.
|
|
124
|
+
expert_name (str): The name of the `expert advisor`.
|
|
125
|
+
expert_id (int): The `unique ID` used to identify the expert advisor
|
|
126
|
+
or the strategy used on the symbol.
|
|
127
|
+
version (str): The `version` of the expert advisor.
|
|
128
|
+
target (float): `Trading period (day, week, month) profit target` in percentage.
|
|
129
|
+
start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
|
|
130
|
+
finishing_time (str): The time after which no new position can be opened.
|
|
131
|
+
ending_time (str): The time after which any open position will be closed.
|
|
132
|
+
verbose (bool | None): If set to None (default), account summary and risk managment
|
|
133
|
+
parameters are printed in the terminal.
|
|
134
|
+
console_log (bool): If set to True, log messages are displayed in the console.
|
|
135
|
+
logger (Logger | str): The logger object to use for logging messages could be a string or a logger object.
|
|
136
|
+
|
|
137
|
+
Inherits:
|
|
138
|
+
- max_risk
|
|
139
|
+
- max_trades
|
|
140
|
+
- rr
|
|
141
|
+
- daily_risk
|
|
142
|
+
- time_frame
|
|
143
|
+
- account_leverage
|
|
144
|
+
- std_stop
|
|
145
|
+
- pchange_sl
|
|
146
|
+
- sl
|
|
147
|
+
- tp
|
|
148
|
+
- be
|
|
149
|
+
See the ``bbstrader.metatrader.risk.RiskManagement`` class for more details on these parameters.
|
|
150
|
+
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
151
|
+
"""
|
|
152
|
+
# Call the parent class constructor first
|
|
153
|
+
super().__init__(
|
|
154
|
+
symbol=symbol,
|
|
155
|
+
start_time=start_time,
|
|
156
|
+
finishing_time=finishing_time,
|
|
157
|
+
**kwargs, # Pass kwargs to the parent constructor
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Initialize Trade-specific attributes
|
|
161
|
+
self.symbol = symbol
|
|
162
|
+
self.expert_name = expert_name
|
|
163
|
+
self.expert_id = expert_id
|
|
164
|
+
self.version = version
|
|
165
|
+
self.target = target
|
|
166
|
+
self.verbose = verbose
|
|
167
|
+
self.start = start_time
|
|
168
|
+
self.end = ending_time
|
|
169
|
+
self.finishing = finishing_time
|
|
170
|
+
self.console_log = console_log
|
|
171
|
+
self.tf = kwargs.get("time_frame", "D1")
|
|
172
|
+
self.kwargs = kwargs
|
|
173
|
+
|
|
174
|
+
self.start_time_hour, self.start_time_minutes = self.start.split(":")
|
|
175
|
+
self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
|
|
176
|
+
":"
|
|
177
|
+
)
|
|
178
|
+
self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
|
|
179
|
+
|
|
180
|
+
self.buy_positions = []
|
|
181
|
+
self.sell_positions = []
|
|
182
|
+
self.opened_positions = []
|
|
183
|
+
self.opened_orders = []
|
|
184
|
+
self.break_even_status = []
|
|
185
|
+
self.break_even_points = {}
|
|
186
|
+
self.trail_after_points = []
|
|
187
|
+
self._retcodes = []
|
|
188
|
+
|
|
189
|
+
self._get_logger(logger, console_log)
|
|
190
|
+
self.initialize(**kwargs)
|
|
191
|
+
self.select_symbol(**kwargs)
|
|
192
|
+
self.prepare_symbol()
|
|
193
|
+
|
|
194
|
+
if self.verbose:
|
|
195
|
+
self.summary()
|
|
196
|
+
time.sleep(1)
|
|
197
|
+
print()
|
|
198
|
+
self.risk_managment()
|
|
199
|
+
print(f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def retcodes(self) -> List[int]:
|
|
203
|
+
"""Return all the retcodes"""
|
|
204
|
+
return self._retcodes
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def logger(self):
|
|
208
|
+
return LOGGER
|
|
209
|
+
|
|
210
|
+
def _get_logger(self, loger: Any, consol_log: bool):
|
|
211
|
+
"""Get the logger object"""
|
|
212
|
+
global LOGGER
|
|
213
|
+
if loger is None:
|
|
214
|
+
... # Do nothing
|
|
215
|
+
elif isinstance(loger, (str, Path)):
|
|
216
|
+
LOGGER = config_logger(f"{BBSTRADER_DIR}/logs/{loger}", consol_log)
|
|
217
|
+
elif isinstance(loger, (Logger, type(log))):
|
|
218
|
+
LOGGER = loger
|
|
219
|
+
|
|
220
|
+
def initialize(self, **kwargs):
|
|
221
|
+
"""
|
|
222
|
+
Initializes the MetaTrader 5 (MT5) terminal for trading operations.
|
|
223
|
+
This method attempts to establish a connection with the MT5 terminal.
|
|
224
|
+
If the initial connection attempt fails due to a timeout, it retries after a specified delay.
|
|
225
|
+
Successful initialization is crucial for the execution of trading operations.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
MT5TerminalError: If initialization fails.
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
if self.verbose:
|
|
232
|
+
print("\nInitializing the basics.")
|
|
233
|
+
check_mt5_connection(**kwargs)
|
|
234
|
+
if self.verbose:
|
|
235
|
+
print(
|
|
236
|
+
f"You are running the @{self.expert_name} Expert advisor,"
|
|
237
|
+
f" Version @{self.version}, on {self.symbol}."
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
LOGGER.error(f"During initialization: {e}")
|
|
241
|
+
|
|
242
|
+
def select_symbol(self, **kwargs):
|
|
243
|
+
"""
|
|
244
|
+
Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
|
|
245
|
+
This method ensures that the specified trading
|
|
246
|
+
symbol is selected and visible in the MT5 terminal,
|
|
247
|
+
allowing subsequent trading operations such as opening and
|
|
248
|
+
closing positions on this symbol.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
MT5TerminalError: If symbole selection fails.
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
check_mt5_connection(**kwargs)
|
|
255
|
+
if not Mt5.symbol_select(self.symbol, True):
|
|
256
|
+
raise_mt5_error(message=INIT_MSG)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
LOGGER.error(f"Selecting symbol '{self.symbol}': {e}")
|
|
259
|
+
|
|
260
|
+
def prepare_symbol(self):
|
|
261
|
+
"""
|
|
262
|
+
Prepares the selected symbol for trading.
|
|
263
|
+
This method checks if the symbol is available and visible in the
|
|
264
|
+
MT5 terminal. If the symbol is not visible, it attempts to select the symbol again.
|
|
265
|
+
This step ensures that trading operations can be performed on the selected symbol without issues.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
MT5TerminalError: If the symbol cannot be made visible for trading operations.
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
symbol_info = self.get_symbol_info(self.symbol)
|
|
272
|
+
if symbol_info is None:
|
|
273
|
+
raise_mt5_error(message=INIT_MSG)
|
|
274
|
+
|
|
275
|
+
if not symbol_info.visible:
|
|
276
|
+
raise_mt5_error(message=INIT_MSG)
|
|
277
|
+
if self.verbose:
|
|
278
|
+
print("Initialization successfully completed.")
|
|
279
|
+
except Exception as e:
|
|
280
|
+
LOGGER.error(f"Preparing symbol '{self.symbol}': {e}")
|
|
281
|
+
|
|
282
|
+
def summary(self):
|
|
283
|
+
"""Show a brief description about the trading program"""
|
|
284
|
+
summary_data = [
|
|
285
|
+
["Expert Advisor Name", f"@{self.expert_name}"],
|
|
286
|
+
["Expert Advisor Version", f"@{self.version}"],
|
|
287
|
+
["Expert | Strategy ID", self.expert_id],
|
|
288
|
+
["Trading Symbol", self.symbol],
|
|
289
|
+
["Trading Time Frame", self.tf],
|
|
290
|
+
["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
|
|
291
|
+
[
|
|
292
|
+
"Finishing Trading Time",
|
|
293
|
+
f"{self.finishing_time_hour}:{self.finishing_time_minutes}",
|
|
294
|
+
],
|
|
295
|
+
[
|
|
296
|
+
"Closing Position After",
|
|
297
|
+
f"{self.ending_time_hour}:{self.ending_time_minutes}",
|
|
298
|
+
],
|
|
299
|
+
]
|
|
300
|
+
# Custom table format
|
|
301
|
+
summary_table = tabulate(
|
|
302
|
+
summary_data, headers=["Summary", "Values"], tablefmt="outline"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Print the table
|
|
306
|
+
print("\n[======= Trade Account Summary =======]")
|
|
307
|
+
print(summary_table)
|
|
308
|
+
|
|
309
|
+
def risk_managment(self):
|
|
310
|
+
"""Show the risk management parameters"""
|
|
311
|
+
|
|
312
|
+
loss = self.currency_risk()["trade_loss"]
|
|
313
|
+
profit = self.currency_risk()["trade_profit"]
|
|
314
|
+
ok = "OK" if self.is_risk_ok() else "Not OK"
|
|
315
|
+
account_info = self.get_account_info()
|
|
316
|
+
_profit = round(self.get_stats()[1]["total_profit"], 2)
|
|
317
|
+
currency = account_info.currency
|
|
318
|
+
rates = self.get_currency_rates(self.symbol)
|
|
319
|
+
marging_currency = rates["mc"]
|
|
320
|
+
account_data = [
|
|
321
|
+
["Account Name", account_info.name],
|
|
322
|
+
["Account Number", account_info.login],
|
|
323
|
+
["Account Server", account_info.server],
|
|
324
|
+
["Account Balance", f"{account_info.balance} {currency}"],
|
|
325
|
+
["Account Profit", f"{_profit} {currency}"],
|
|
326
|
+
["Account Equity", f"{account_info.equity} {currency}"],
|
|
327
|
+
["Account Leverage", self.get_leverage(True)],
|
|
328
|
+
["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
|
|
329
|
+
["Account Free Margin", f"{account_info.margin_free} {currency}"],
|
|
330
|
+
["Maximum Drawdown", f"{self.max_risk}%"],
|
|
331
|
+
["Risk Allowed", f"{round((self.max_risk - self.risk_level()), 2)}%"],
|
|
332
|
+
["Volume", f"{self.volume()} {marging_currency}"],
|
|
333
|
+
["Risk Per trade", f"{-self.get_currency_risk()} {currency}"],
|
|
334
|
+
["Profit Expected Per trade", f"{self.expected_profit()} {currency}"],
|
|
335
|
+
["Lot Size", f"{self.get_lot()} Lots"],
|
|
336
|
+
["Stop Loss", f"{self.get_stop_loss()} Points"],
|
|
337
|
+
["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
|
|
338
|
+
["Take Profit", f"{self.get_take_profit()} Points"],
|
|
339
|
+
["Profit Value Per Tick", f"{round(profit, 5)} {currency}"],
|
|
340
|
+
["Break Even", f"{self.get_break_even()} Points"],
|
|
341
|
+
["Deviation", f"{self.get_deviation()} Points"],
|
|
342
|
+
["Trading Time Interval", f"{self.get_minutes()} Minutes"],
|
|
343
|
+
["Risk Level", ok],
|
|
344
|
+
["Maximum Trades", self.max_trade()],
|
|
345
|
+
]
|
|
346
|
+
# Custom table format
|
|
347
|
+
print("\n[======= Account Risk Management Overview =======]")
|
|
348
|
+
table = tabulate(
|
|
349
|
+
account_data, headers=["Risk Metrics", "Values"], tablefmt="outline"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Print the table
|
|
353
|
+
print(table)
|
|
354
|
+
|
|
355
|
+
def statistics(self, save=True, dir=None):
|
|
356
|
+
"""
|
|
357
|
+
Print some statistics for the trading session and save to CSV if specified.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
save (bool, optional): Whether to save the statistics to a CSV file.
|
|
361
|
+
dir (str, optional): The directory to save the CSV file.
|
|
362
|
+
"""
|
|
363
|
+
stats, additional_stats = self.get_stats()
|
|
364
|
+
|
|
365
|
+
profit = round(stats["profit"], 2)
|
|
366
|
+
win_rate = stats["win_rate"]
|
|
367
|
+
total_fees = round(stats["total_fees"], 3)
|
|
368
|
+
average_fee = round(stats["average_fee"], 3)
|
|
369
|
+
currency = self.get_account_info().currency
|
|
370
|
+
net_profit = round((profit + total_fees), 2)
|
|
371
|
+
trade_risk = round(self.get_currency_risk() * -1, 2)
|
|
372
|
+
|
|
373
|
+
# Formatting the statistics output
|
|
374
|
+
session_data = [
|
|
375
|
+
["Total Trades", stats["deals"]],
|
|
376
|
+
["Winning Trades", stats["win_trades"]],
|
|
377
|
+
["Losing Trades", stats["loss_trades"]],
|
|
378
|
+
["Session Profit", f"{profit} {currency}"],
|
|
379
|
+
["Total Fees", f"{total_fees} {currency}"],
|
|
380
|
+
["Average Fees", f"{average_fee} {currency}"],
|
|
381
|
+
["Net Profit", f"{net_profit} {currency}"],
|
|
382
|
+
["Risk per Trade", f"{trade_risk} {currency}"],
|
|
383
|
+
["Expected Profit per Trade", f"{self.expected_profit()} {currency}"],
|
|
384
|
+
["Risk Reward Ratio", self.rr],
|
|
385
|
+
["Win Rate", f"{win_rate}%"],
|
|
386
|
+
["Sharpe Ratio", self.sharpe()],
|
|
387
|
+
["Trade Profitability", additional_stats["profitability"]],
|
|
388
|
+
]
|
|
389
|
+
session_table = tabulate(
|
|
390
|
+
session_data, headers=["Statistics", "Values"], tablefmt="outline"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Print the formatted statistics
|
|
394
|
+
if self.verbose:
|
|
395
|
+
print("\n[======= Trading Session Statistics =======]")
|
|
396
|
+
print(session_table)
|
|
397
|
+
|
|
398
|
+
# Save to CSV if specified
|
|
399
|
+
if save:
|
|
400
|
+
today_date = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
401
|
+
# Create a dictionary with the statistics
|
|
402
|
+
statistics_dict = {item[0]: item[1] for item in session_data}
|
|
403
|
+
stats_df = pd.DataFrame(statistics_dict, index=[0])
|
|
404
|
+
# Create the directory if it doesn't exist
|
|
405
|
+
dir = dir or ".sessions"
|
|
406
|
+
os.makedirs(dir, exist_ok=True)
|
|
407
|
+
if "." in self.symbol:
|
|
408
|
+
symbol = self.symbol.split(".")[0]
|
|
409
|
+
else:
|
|
410
|
+
symbol = self.symbol
|
|
411
|
+
|
|
412
|
+
filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
|
|
413
|
+
filepath = os.path.join(dir, filename)
|
|
414
|
+
stats_df.to_csv(filepath, index=False)
|
|
415
|
+
LOGGER.info(f"Session statistics saved to {filepath}")
|
|
416
|
+
|
|
417
|
+
Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
418
|
+
|
|
419
|
+
def open_buy_position(
|
|
420
|
+
self,
|
|
421
|
+
action: Buys = "BMKT",
|
|
422
|
+
price: Optional[float] = None,
|
|
423
|
+
stoplimit: Optional[float] = None,
|
|
424
|
+
mm: bool = True,
|
|
425
|
+
trail: bool = True,
|
|
426
|
+
id: Optional[int] = None,
|
|
427
|
+
comment: Optional[str] = None,
|
|
428
|
+
symbol: Optional[str] = None,
|
|
429
|
+
volume: Optional[float] = None,
|
|
430
|
+
sl: Optional[float] = None,
|
|
431
|
+
tp: Optional[float] = None,
|
|
432
|
+
):
|
|
433
|
+
"""
|
|
434
|
+
Open a Buy positin
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
action (str): `BMKT` for Market orders or `BLMT`,
|
|
438
|
+
`BSTP`,`BSTPLMT` for pending orders
|
|
439
|
+
price (float): The price at which to open an order
|
|
440
|
+
stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
|
|
441
|
+
The pending order is not passed to the trading system until that moment
|
|
442
|
+
id (int): The strategy id or expert Id
|
|
443
|
+
mm (bool): Weither to put stop loss and tp or not
|
|
444
|
+
trail (bool): Weither to trail the stop loss or not
|
|
445
|
+
comment (str): The comment for the opening position
|
|
446
|
+
"""
|
|
447
|
+
Id = id if id is not None else self.expert_id
|
|
448
|
+
point = self.get_symbol_info(self.symbol).point
|
|
449
|
+
if action != "BMKT":
|
|
450
|
+
if price is not None:
|
|
451
|
+
_price = price
|
|
452
|
+
else:
|
|
453
|
+
raise ValueError("You need to set a price for pending orders")
|
|
454
|
+
else:
|
|
455
|
+
_price = self.get_tick_info(self.symbol).ask
|
|
456
|
+
|
|
457
|
+
lot = volume or self.get_lot()
|
|
458
|
+
stop_loss = self.get_stop_loss()
|
|
459
|
+
take_profit = self.get_take_profit()
|
|
460
|
+
deviation = self.get_deviation()
|
|
461
|
+
request = {
|
|
462
|
+
"action": Mt5.TRADE_ACTION_DEAL,
|
|
463
|
+
"symbol": symbol or self.symbol,
|
|
464
|
+
"volume": float(lot),
|
|
465
|
+
"type": Mt5.ORDER_TYPE_BUY,
|
|
466
|
+
"price": _price,
|
|
467
|
+
"deviation": deviation,
|
|
468
|
+
"magic": Id,
|
|
469
|
+
"comment": comment or f"@{self.expert_name}",
|
|
470
|
+
"type_time": Mt5.ORDER_TIME_GTC,
|
|
471
|
+
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
472
|
+
}
|
|
473
|
+
mm_price = _price
|
|
474
|
+
if action != "BMKT":
|
|
475
|
+
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
476
|
+
request["type"] = self._order_type()[action][0]
|
|
477
|
+
if action == "BSTPLMT":
|
|
478
|
+
if stoplimit is None:
|
|
479
|
+
raise ValueError("You need to set a stoplimit price for BSTPLMT orders")
|
|
480
|
+
if stoplimit > _price:
|
|
481
|
+
raise ValueError(
|
|
482
|
+
"Stoplimit price must be less than the price and greater than the current price"
|
|
483
|
+
)
|
|
484
|
+
request["stoplimit"] = stoplimit
|
|
485
|
+
mm_price = stoplimit
|
|
486
|
+
if mm:
|
|
487
|
+
request["sl"] = sl or mm_price - stop_loss * point
|
|
488
|
+
request["tp"] = tp or mm_price + take_profit * point
|
|
489
|
+
self.break_even(mm=mm, id=Id, trail=trail)
|
|
490
|
+
if self.check(comment):
|
|
491
|
+
return self.request_result(_price, request, action)
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
def _order_type(self):
|
|
495
|
+
type = {
|
|
496
|
+
"BMKT": (Mt5.ORDER_TYPE_BUY, "BUY"),
|
|
497
|
+
"SMKT": (Mt5.ORDER_TYPE_BUY, "SELL"),
|
|
498
|
+
"BLMT": (Mt5.ORDER_TYPE_BUY_LIMIT, "BUY_LIMIT"),
|
|
499
|
+
"SLMT": (Mt5.ORDER_TYPE_SELL_LIMIT, "SELL_LIMIT"),
|
|
500
|
+
"BSTP": (Mt5.ORDER_TYPE_BUY_STOP, "BUY_STOP"),
|
|
501
|
+
"SSTP": (Mt5.ORDER_TYPE_SELL_STOP, "SELL_STOP"),
|
|
502
|
+
"BSTPLMT": (Mt5.ORDER_TYPE_BUY_STOP_LIMIT, "BUY_STOP_LIMIT"),
|
|
503
|
+
"SSTPLMT": (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, "SELL_STOP_LIMIT"),
|
|
504
|
+
}
|
|
505
|
+
return type
|
|
506
|
+
|
|
507
|
+
Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
508
|
+
|
|
509
|
+
def open_sell_position(
|
|
510
|
+
self,
|
|
511
|
+
action: Sells = "SMKT",
|
|
512
|
+
price: Optional[float] = None,
|
|
513
|
+
stoplimit: Optional[float] = None,
|
|
514
|
+
mm: bool = True,
|
|
515
|
+
trail: bool = True,
|
|
516
|
+
id: Optional[int] = None,
|
|
517
|
+
comment: Optional[str] = None,
|
|
518
|
+
symbol: Optional[str] = None,
|
|
519
|
+
volume: Optional[float] = None,
|
|
520
|
+
sl: Optional[float] = None,
|
|
521
|
+
tp: Optional[float] = None,
|
|
522
|
+
):
|
|
523
|
+
"""
|
|
524
|
+
Open a sell positin
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
action (str): `SMKT` for Market orders
|
|
528
|
+
or ``SLMT``, ``SSTP``,``SSTPLMT`` for pending orders
|
|
529
|
+
price (float): The price at which to open an order
|
|
530
|
+
stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
|
|
531
|
+
The pending order is not passed to the trading system until that moment
|
|
532
|
+
id (int): The strategy id or expert Id
|
|
533
|
+
mm (bool): Weither to put stop loss and tp or not
|
|
534
|
+
trail (bool): Weither to trail the stop loss or not
|
|
535
|
+
comment (str): The comment for the closing position
|
|
536
|
+
symbol (str): The symbol to trade
|
|
537
|
+
volume (float): The volume (lot) to trade
|
|
538
|
+
sl (float): The stop loss in points
|
|
539
|
+
tp (float): The take profit in points
|
|
540
|
+
"""
|
|
541
|
+
Id = id if id is not None else self.expert_id
|
|
542
|
+
point = self.get_symbol_info(self.symbol).point
|
|
543
|
+
if action != "SMKT":
|
|
544
|
+
if price is not None:
|
|
545
|
+
_price = price
|
|
546
|
+
else:
|
|
547
|
+
raise ValueError("You need to set a price for pending orders")
|
|
548
|
+
else:
|
|
549
|
+
_price = self.get_tick_info(self.symbol).bid
|
|
550
|
+
|
|
551
|
+
lot = volume or self.get_lot()
|
|
552
|
+
stop_loss = self.get_stop_loss()
|
|
553
|
+
take_profit = self.get_take_profit()
|
|
554
|
+
deviation = self.get_deviation()
|
|
555
|
+
request = {
|
|
556
|
+
"action": Mt5.TRADE_ACTION_DEAL,
|
|
557
|
+
"symbol": symbol or self.symbol,
|
|
558
|
+
"volume": float(lot),
|
|
559
|
+
"type": Mt5.ORDER_TYPE_SELL,
|
|
560
|
+
"price": _price,
|
|
561
|
+
"deviation": deviation,
|
|
562
|
+
"magic": Id,
|
|
563
|
+
"comment": comment or f"@{self.expert_name}",
|
|
564
|
+
"type_time": Mt5.ORDER_TIME_GTC,
|
|
565
|
+
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
566
|
+
}
|
|
567
|
+
mm_price = _price
|
|
568
|
+
if action != "SMKT":
|
|
569
|
+
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
570
|
+
request["type"] = self._order_type()[action][0]
|
|
571
|
+
if action == "SSTPLMT":
|
|
572
|
+
if stoplimit is None:
|
|
573
|
+
raise ValueError("You need to set a stoplimit price for SSTPLMT orders")
|
|
574
|
+
if stoplimit < _price:
|
|
575
|
+
raise ValueError(
|
|
576
|
+
"Stoplimit price must be greater than the price and less than the current price"
|
|
577
|
+
)
|
|
578
|
+
request["stoplimit"] = stoplimit
|
|
579
|
+
mm_price = stoplimit
|
|
580
|
+
if mm:
|
|
581
|
+
request["sl"] = sl or mm_price + stop_loss * point
|
|
582
|
+
request["tp"] = tp or mm_price - take_profit * point
|
|
583
|
+
self.break_even(mm=mm, id=Id, trail=trail)
|
|
584
|
+
if self.check(comment):
|
|
585
|
+
return self.request_result(_price, request, action)
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
def check(self, comment):
|
|
589
|
+
"""
|
|
590
|
+
Verify if all conditions for taking a position are valide,
|
|
591
|
+
These conditions are based on the Maximum risk ,daily risk,
|
|
592
|
+
the starting, the finishing, and ending trading time.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
comment (str): The comment for the closing position
|
|
596
|
+
"""
|
|
597
|
+
if self.days_end():
|
|
598
|
+
return False
|
|
599
|
+
elif not self.trading_time():
|
|
600
|
+
LOGGER.info(f"Not Trading time, SYMBOL={self.symbol}")
|
|
601
|
+
return False
|
|
602
|
+
elif not self.is_risk_ok():
|
|
603
|
+
LOGGER.error(f"Account Risk not allowed, SYMBOL={self.symbol}")
|
|
604
|
+
self._check(comment)
|
|
605
|
+
return False
|
|
606
|
+
elif self.profit_target():
|
|
607
|
+
self._check(f"Profit target Reached !!! SYMBOL={self.symbol}")
|
|
608
|
+
return True
|
|
609
|
+
|
|
610
|
+
def _check(self, txt: str = ""):
|
|
611
|
+
if (
|
|
612
|
+
self.positive_profit(id=self.expert_id)
|
|
613
|
+
or self.get_current_positions() is None
|
|
614
|
+
):
|
|
615
|
+
self.close_positions(position_type="all")
|
|
616
|
+
LOGGER.info(txt)
|
|
617
|
+
time.sleep(5)
|
|
618
|
+
self.statistics(save=True)
|
|
619
|
+
|
|
620
|
+
def request_result(self, price: float, request: Dict[str, Any], type: Buys | Sells):
|
|
621
|
+
"""
|
|
622
|
+
Check if a trading order has been sent correctly
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
price (float): Price for opening the position
|
|
626
|
+
request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
|
|
627
|
+
all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
|
|
628
|
+
|
|
629
|
+
type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
|
|
630
|
+
"""
|
|
631
|
+
# Send a trading request
|
|
632
|
+
# Check the execution result
|
|
633
|
+
pos = self._order_type()[type][1]
|
|
634
|
+
addtionnal = f", SYMBOL={self.symbol}"
|
|
635
|
+
try:
|
|
636
|
+
self.check_order(request)
|
|
637
|
+
result = self.send_order(request)
|
|
638
|
+
except Exception as e:
|
|
639
|
+
print(f"{self.current_datetime()} -", end=" ")
|
|
640
|
+
trade_retcode_message(
|
|
641
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
642
|
+
)
|
|
643
|
+
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
644
|
+
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
645
|
+
for fill in FILLING_TYPE:
|
|
646
|
+
request["type_filling"] = fill
|
|
647
|
+
result = self.send_order(request)
|
|
648
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
649
|
+
break
|
|
650
|
+
elif result.retcode == Mt5.TRADE_RETCODE_INVALID_VOLUME: # 10014
|
|
651
|
+
new_volume = int(request["volume"])
|
|
652
|
+
if new_volume >= 1:
|
|
653
|
+
request["volume"] = new_volume
|
|
654
|
+
result = self.send_order(request)
|
|
655
|
+
elif result.retcode not in self._retcodes:
|
|
656
|
+
self._retcodes.append(result.retcode)
|
|
657
|
+
msg = trade_retcode_message(result.retcode)
|
|
658
|
+
LOGGER.error(
|
|
659
|
+
f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
660
|
+
)
|
|
661
|
+
elif result.retcode in [
|
|
662
|
+
Mt5.TRADE_RETCODE_CONNECTION,
|
|
663
|
+
Mt5.TRADE_RETCODE_TIMEOUT,
|
|
664
|
+
]:
|
|
665
|
+
tries = 0
|
|
666
|
+
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
667
|
+
time.sleep(1)
|
|
668
|
+
try:
|
|
669
|
+
self.check_order(request)
|
|
670
|
+
result = self.send_order(request)
|
|
671
|
+
except Exception as e:
|
|
672
|
+
print(f"{self.current_datetime()} -", end=" ")
|
|
673
|
+
trade_retcode_message(
|
|
674
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
675
|
+
)
|
|
676
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
677
|
+
break
|
|
678
|
+
tries += 1
|
|
679
|
+
# Print the result
|
|
680
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
681
|
+
msg = trade_retcode_message(result.retcode)
|
|
682
|
+
LOGGER.info(f"Trade Order {msg}{addtionnal}")
|
|
683
|
+
if type != "BMKT" or type != "SMKT":
|
|
684
|
+
self.opened_orders.append(result.order)
|
|
685
|
+
long_msg = (
|
|
686
|
+
f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{price}, "
|
|
687
|
+
f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
|
|
688
|
+
f"Tp: {self.get_take_profit()}"
|
|
689
|
+
)
|
|
690
|
+
LOGGER.info(long_msg)
|
|
691
|
+
time.sleep(0.1)
|
|
692
|
+
if type == "BMKT" or type == "SMKT":
|
|
693
|
+
self.opened_positions.append(result.order)
|
|
694
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
695
|
+
if positions is not None:
|
|
696
|
+
for position in positions:
|
|
697
|
+
if position.ticket == result.order:
|
|
698
|
+
if position.type == 0:
|
|
699
|
+
order_type = "BUY"
|
|
700
|
+
self.buy_positions.append(position.ticket)
|
|
701
|
+
else:
|
|
702
|
+
order_type = "SELL"
|
|
703
|
+
self.sell_positions.append(position.ticket)
|
|
704
|
+
profit = round(self.get_account_info().profit, 5)
|
|
705
|
+
order_info = (
|
|
706
|
+
f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open, 5)}, "
|
|
707
|
+
f"Sl: @{position.sl} Tp: @{position.tp}"
|
|
708
|
+
)
|
|
709
|
+
LOGGER.info(order_info)
|
|
710
|
+
pos_info = (
|
|
711
|
+
f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
|
|
712
|
+
f"{self.get_account_info().currency}]\n"
|
|
713
|
+
)
|
|
714
|
+
LOGGER.info(pos_info)
|
|
715
|
+
return True
|
|
716
|
+
else:
|
|
717
|
+
msg = trade_retcode_message(result.retcode)
|
|
718
|
+
LOGGER.error(
|
|
719
|
+
f"Unable to Open Position, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
720
|
+
)
|
|
721
|
+
return False
|
|
722
|
+
|
|
723
|
+
def open_position(
|
|
724
|
+
self,
|
|
725
|
+
action: Buys | Sells,
|
|
726
|
+
price: Optional[float] = None,
|
|
727
|
+
stoplimit: Optional[float] = None,
|
|
728
|
+
id: Optional[int] = None,
|
|
729
|
+
mm: bool = True,
|
|
730
|
+
trail: bool = True,
|
|
731
|
+
comment: Optional[str] = None,
|
|
732
|
+
symbol: Optional[str] = None,
|
|
733
|
+
volume: Optional[float] = None,
|
|
734
|
+
sl: Optional[float] = None,
|
|
735
|
+
tp: Optional[float] = None,
|
|
736
|
+
):
|
|
737
|
+
"""
|
|
738
|
+
Open a buy or sell position.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
action (str): (`'BMKT'`, `'SMKT'`) for Market orders
|
|
742
|
+
or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
|
|
743
|
+
price (float): The price at which to open an order
|
|
744
|
+
stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
|
|
745
|
+
The pending order is not passed to the trading system until that moment
|
|
746
|
+
id (int): The strategy id or expert Id
|
|
747
|
+
mm (bool): Weither to put stop loss and tp or not
|
|
748
|
+
trail (bool): Weither to trail the stop loss or not
|
|
749
|
+
comment (str): The comment for the closing position
|
|
750
|
+
symbol (str): The symbol to trade
|
|
751
|
+
volume (float): The volume (lot) to trade
|
|
752
|
+
sl (float): The stop loss in points
|
|
753
|
+
tp (float): The take profit in points
|
|
754
|
+
"""
|
|
755
|
+
BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
756
|
+
SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
757
|
+
if action in BUYS:
|
|
758
|
+
return self.open_buy_position(
|
|
759
|
+
action=action,
|
|
760
|
+
price=price,
|
|
761
|
+
stoplimit=stoplimit,
|
|
762
|
+
id=id,
|
|
763
|
+
mm=mm,
|
|
764
|
+
trail=trail,
|
|
765
|
+
comment=comment,
|
|
766
|
+
symbol=symbol,
|
|
767
|
+
volume=volume,
|
|
768
|
+
sl=sl,
|
|
769
|
+
tp=tp,
|
|
770
|
+
)
|
|
771
|
+
elif action in SELLS:
|
|
772
|
+
return self.open_sell_position(
|
|
773
|
+
action=action,
|
|
774
|
+
price=price,
|
|
775
|
+
stoplimit=stoplimit,
|
|
776
|
+
id=id,
|
|
777
|
+
mm=mm,
|
|
778
|
+
trail=trail,
|
|
779
|
+
comment=comment,
|
|
780
|
+
symbol=symbol,
|
|
781
|
+
volume=volume,
|
|
782
|
+
sl=sl,
|
|
783
|
+
tp=tp,
|
|
784
|
+
)
|
|
785
|
+
else:
|
|
786
|
+
raise ValueError(
|
|
787
|
+
f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}"
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
@property
|
|
791
|
+
def orders(self):
|
|
792
|
+
"""Return all opened order's tickets"""
|
|
793
|
+
if len(self.opened_orders) != 0:
|
|
794
|
+
return self.opened_orders
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
@property
|
|
798
|
+
def positions(self):
|
|
799
|
+
"""Return all opened position's tickets"""
|
|
800
|
+
if len(self.opened_positions) != 0:
|
|
801
|
+
return self.opened_positions
|
|
802
|
+
return None
|
|
803
|
+
|
|
804
|
+
@property
|
|
805
|
+
def buypos(self):
|
|
806
|
+
"""Return all buy opened position's tickets"""
|
|
807
|
+
if len(self.buy_positions) != 0:
|
|
808
|
+
return self.buy_positions
|
|
809
|
+
return None
|
|
810
|
+
|
|
811
|
+
@property
|
|
812
|
+
def sellpos(self):
|
|
813
|
+
"""Return all sell opened position's tickets"""
|
|
814
|
+
if len(self.sell_positions) != 0:
|
|
815
|
+
return self.sell_positions
|
|
816
|
+
return None
|
|
817
|
+
|
|
818
|
+
@property
|
|
819
|
+
def bepos(self):
|
|
820
|
+
"""Return All positon's tickets
|
|
821
|
+
for which a break even has been set"""
|
|
822
|
+
if len(self.break_even_status) != 0:
|
|
823
|
+
return self.break_even_status
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
def get_filtered_tickets(
|
|
827
|
+
self, id: Optional[int] = None, filter_type: Optional[str] = None, th=None
|
|
828
|
+
) -> List[int] | None:
|
|
829
|
+
"""
|
|
830
|
+
Get tickets for positions or orders based on filters.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
id (int): The strategy id or expert Id
|
|
834
|
+
filter_type (str): Filter type to apply on the tickets,
|
|
835
|
+
- `orders` are current open orders
|
|
836
|
+
- `buy_stops` are current buy stop orders
|
|
837
|
+
- `sell_stops` are current sell stop orders
|
|
838
|
+
- `buy_limits` are current buy limit orders
|
|
839
|
+
- `sell_limits` are current sell limit orders
|
|
840
|
+
- `buy_stop_limits` are current buy stop limit orders
|
|
841
|
+
- `sell_stop_limits` are current sell stop limit orders
|
|
842
|
+
- `positions` are all current open positions
|
|
843
|
+
- `buys` and `sells` are current buy or sell open positions
|
|
844
|
+
- `profitables` are current open position that have a profit greater than a threshold
|
|
845
|
+
- `losings` are current open position that have a negative profit
|
|
846
|
+
th (bool): the minimum treshold for winning position
|
|
847
|
+
(only relevant when filter_type is 'profitables')
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
List[int] | None: A list of filtered tickets
|
|
851
|
+
or None if no tickets match the criteria.
|
|
852
|
+
"""
|
|
853
|
+
Id = id if id is not None else self.expert_id
|
|
854
|
+
POSITIONS = ["positions", "buys", "sells", "profitables", "losings"]
|
|
855
|
+
|
|
856
|
+
if filter_type not in POSITIONS:
|
|
857
|
+
items = self.get_orders(symbol=self.symbol)
|
|
858
|
+
else:
|
|
859
|
+
items = self.get_positions(symbol=self.symbol)
|
|
860
|
+
|
|
861
|
+
filtered_tickets = []
|
|
862
|
+
|
|
863
|
+
if items is not None:
|
|
864
|
+
for item in items:
|
|
865
|
+
if item.magic == Id:
|
|
866
|
+
if filter_type == "buys" and item.type != 0:
|
|
867
|
+
continue
|
|
868
|
+
if filter_type == "sells" and item.type != 1:
|
|
869
|
+
continue
|
|
870
|
+
if filter_type == "losings" and item.profit > 0:
|
|
871
|
+
continue
|
|
872
|
+
if filter_type == "profitables" and not self.win_trade(item, th=th):
|
|
873
|
+
continue
|
|
874
|
+
if (
|
|
875
|
+
filter_type == "buy_stops"
|
|
876
|
+
and item.type != self._order_type()["BSTP"][0]
|
|
877
|
+
):
|
|
878
|
+
continue
|
|
879
|
+
if (
|
|
880
|
+
filter_type == "sell_stops"
|
|
881
|
+
and item.type != self._order_type()["SSTP"][0]
|
|
882
|
+
):
|
|
883
|
+
continue
|
|
884
|
+
if (
|
|
885
|
+
filter_type == "buy_limits"
|
|
886
|
+
and item.type != self._order_type()["BLMT"][0]
|
|
887
|
+
):
|
|
888
|
+
continue
|
|
889
|
+
if (
|
|
890
|
+
filter_type == "sell_limits"
|
|
891
|
+
and item.type != self._order_type()["SLMT"][0]
|
|
892
|
+
):
|
|
893
|
+
continue
|
|
894
|
+
if (
|
|
895
|
+
filter_type == "buy_stop_limits"
|
|
896
|
+
and item.type != self._order_type()["BSTPLMT"][0]
|
|
897
|
+
):
|
|
898
|
+
continue
|
|
899
|
+
if (
|
|
900
|
+
filter_type == "sell_stop_limits"
|
|
901
|
+
and item.type != self._order_type()["SSTPLMT"][0]
|
|
902
|
+
):
|
|
903
|
+
continue
|
|
904
|
+
filtered_tickets.append(item.ticket)
|
|
905
|
+
return filtered_tickets if filtered_tickets else None
|
|
906
|
+
return None
|
|
907
|
+
|
|
908
|
+
def get_current_orders(self, id: Optional[int] = None) -> List[int] | None:
|
|
909
|
+
return self.get_filtered_tickets(id=id, filter_type="orders")
|
|
910
|
+
|
|
911
|
+
def get_current_buy_stops(self, id: Optional[int] = None) -> List[int] | None:
|
|
912
|
+
return self.get_filtered_tickets(id=id, filter_type="buy_stops")
|
|
913
|
+
|
|
914
|
+
def get_current_sell_stops(self, id: Optional[int] = None) -> List[int] | None:
|
|
915
|
+
return self.get_filtered_tickets(id=id, filter_type="sell_stops")
|
|
916
|
+
|
|
917
|
+
def get_current_buy_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
918
|
+
return self.get_filtered_tickets(id=id, filter_type="buy_limits")
|
|
919
|
+
|
|
920
|
+
def get_current_sell_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
921
|
+
return self.get_filtered_tickets(id=id, filter_type="sell_limits")
|
|
922
|
+
|
|
923
|
+
def get_current_buy_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
924
|
+
return self.get_filtered_tickets(id=id, filter_type="buy_stop_limits")
|
|
925
|
+
|
|
926
|
+
def get_current_sell_stop_limits(
|
|
927
|
+
self, id: Optional[int] = None
|
|
928
|
+
) -> List[int] | None:
|
|
929
|
+
return self.get_filtered_tickets(id=id, filter_type="sell_stop_limits")
|
|
930
|
+
|
|
931
|
+
def get_current_positions(self, id: Optional[int] = None) -> List[int] | None:
|
|
932
|
+
return self.get_filtered_tickets(id=id, filter_type="positions")
|
|
933
|
+
|
|
934
|
+
def get_current_profitables(
|
|
935
|
+
self, id: Optional[int] = None, th=None
|
|
936
|
+
) -> List[int] | None:
|
|
937
|
+
return self.get_filtered_tickets(id=id, filter_type="profitables", th=th)
|
|
938
|
+
|
|
939
|
+
def get_current_losings(self, id: Optional[int] = None) -> List[int] | None:
|
|
940
|
+
return self.get_filtered_tickets(id=id, filter_type="losings")
|
|
941
|
+
|
|
942
|
+
def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
|
|
943
|
+
return self.get_filtered_tickets(id=id, filter_type="buys")
|
|
944
|
+
|
|
945
|
+
def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
|
|
946
|
+
return self.get_filtered_tickets(id=id, filter_type="sells")
|
|
947
|
+
|
|
948
|
+
def positive_profit(
|
|
949
|
+
self, th: Optional[float] = None, id: Optional[int] = None, account: bool = True
|
|
950
|
+
) -> bool:
|
|
951
|
+
"""
|
|
952
|
+
Check is the total profit on current open positions
|
|
953
|
+
Is greater than a minimum profit express as percentage
|
|
954
|
+
of the profit target.
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
th (float): The minimum profit target on current positions
|
|
958
|
+
id (int): The strategy id or expert Id
|
|
959
|
+
account (bool): Weither to check positions on the account or on the symbol
|
|
960
|
+
"""
|
|
961
|
+
if account and id is None:
|
|
962
|
+
# All open positions no matter the symbol or strategy or expert
|
|
963
|
+
positions = self.get_positions()
|
|
964
|
+
elif account and id is not None:
|
|
965
|
+
# All open positions for a specific strategy or expert no matter the symbol
|
|
966
|
+
positions = self.get_positions()
|
|
967
|
+
if positions is not None:
|
|
968
|
+
positions = [position for position in positions if position.magic == id]
|
|
969
|
+
elif not account and id is None:
|
|
970
|
+
# All open positions for the current symbol no matter the strategy or expert
|
|
971
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
972
|
+
elif not account and id is not None:
|
|
973
|
+
# All open positions for the current symbol and a specific strategy or expert
|
|
974
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
975
|
+
if positions is not None:
|
|
976
|
+
positions = [position for position in positions if position.magic == id]
|
|
977
|
+
|
|
978
|
+
if positions is not None:
|
|
979
|
+
profit = 0.0
|
|
980
|
+
balance = self.get_account_info().balance
|
|
981
|
+
target = round((balance * self.target) / 100, 2)
|
|
982
|
+
for position in positions:
|
|
983
|
+
profit += position.profit
|
|
984
|
+
fees = self.get_stats()[0]["average_fee"] * len(positions)
|
|
985
|
+
current_profit = profit + fees
|
|
986
|
+
th_profit = (target * th) / 100 if th is not None else (target * 0.01)
|
|
987
|
+
return current_profit >= th_profit
|
|
988
|
+
return False
|
|
989
|
+
|
|
990
|
+
def _get_trail_after_points(self, trail_after_points: int | str) -> int:
|
|
991
|
+
if isinstance(trail_after_points, str):
|
|
992
|
+
if trail_after_points == "SL":
|
|
993
|
+
return self.get_stop_loss()
|
|
994
|
+
elif trail_after_points == "TP":
|
|
995
|
+
return self.get_take_profit()
|
|
996
|
+
elif trail_after_points == "BE":
|
|
997
|
+
return self.get_break_even()
|
|
998
|
+
# TODO: Add other combinations (e.g. "SL+TP", "SL+BE", "TP+BE", "SL*N", etc.)
|
|
999
|
+
return trail_after_points
|
|
1000
|
+
|
|
1001
|
+
def break_even(
|
|
1002
|
+
self,
|
|
1003
|
+
mm=True,
|
|
1004
|
+
id: Optional[int] = None,
|
|
1005
|
+
trail: Optional[bool] = True,
|
|
1006
|
+
stop_trail: int | str = None,
|
|
1007
|
+
trail_after_points: int | str = None,
|
|
1008
|
+
be_plus_points: Optional[int] = None,
|
|
1009
|
+
):
|
|
1010
|
+
"""
|
|
1011
|
+
Manages the break-even level of a trading position.
|
|
1012
|
+
|
|
1013
|
+
This function checks whether it is time to set a break-even stop loss for an open position.
|
|
1014
|
+
If the break-even level is already set, it monitors price movement and updates the stop loss
|
|
1015
|
+
accordingly if the `trail` parameter is enabled.
|
|
1016
|
+
|
|
1017
|
+
When `trail` is enabled, the function dynamically adjusts the break-even level based on the
|
|
1018
|
+
`trail_after_points` and `stop_trail` parameters.
|
|
1019
|
+
|
|
1020
|
+
Args:
|
|
1021
|
+
id (int): The strategy ID or expert ID.
|
|
1022
|
+
mm (bool): Whether to manage the position or not.
|
|
1023
|
+
trail (bool): Whether to trail the stop loss or not.
|
|
1024
|
+
stop_trail (int): Number of points to trail the stop loss by.
|
|
1025
|
+
It represent the distance from the current price to the stop loss.
|
|
1026
|
+
trail_after_points (int, str): Number of points in profit
|
|
1027
|
+
from where the strategy will start to trail the stop loss.
|
|
1028
|
+
If set to str, it must be one of the following values:
|
|
1029
|
+
- 'SL' to trail the stop loss after the profit reaches the stop loss level in points.
|
|
1030
|
+
- 'TP' to trail the stop loss after the profit reaches the take profit level in points.
|
|
1031
|
+
- 'BE' to trail the stop loss after the profit reaches the break-even level in points.
|
|
1032
|
+
be_plus_points (int): Number of points to add to the break-even level.
|
|
1033
|
+
Represents the minimum profit to secure.
|
|
1034
|
+
"""
|
|
1035
|
+
time.sleep(0.1)
|
|
1036
|
+
if not mm:
|
|
1037
|
+
return
|
|
1038
|
+
Id = id if id is not None else self.expert_id
|
|
1039
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
1040
|
+
be = self.get_break_even()
|
|
1041
|
+
if trail_after_points is not None:
|
|
1042
|
+
if isinstance(trail_after_points, int):
|
|
1043
|
+
assert trail_after_points > be, (
|
|
1044
|
+
"trail_after_points must be greater than break even or set to None"
|
|
1045
|
+
)
|
|
1046
|
+
trail_after_points = self._get_trail_after_points(trail_after_points)
|
|
1047
|
+
if positions is not None:
|
|
1048
|
+
for position in positions:
|
|
1049
|
+
if position.magic == Id:
|
|
1050
|
+
symbol_info = self.get_symbol_info(self.symbol)
|
|
1051
|
+
size = symbol_info.trade_tick_size
|
|
1052
|
+
value = symbol_info.trade_tick_value
|
|
1053
|
+
point = symbol_info.point
|
|
1054
|
+
digits = symbol_info.digits
|
|
1055
|
+
points = position.profit * (size / value / position.volume)
|
|
1056
|
+
break_even = float(points / point) >= be
|
|
1057
|
+
if break_even:
|
|
1058
|
+
# Check if break-even has already been set for this position
|
|
1059
|
+
if position.ticket not in self.break_even_status:
|
|
1060
|
+
price = None
|
|
1061
|
+
if be_plus_points is not None:
|
|
1062
|
+
price = position.price_open + (be_plus_points * point)
|
|
1063
|
+
self.set_break_even(position, be, price=price)
|
|
1064
|
+
self.break_even_status.append(position.ticket)
|
|
1065
|
+
self.break_even_points[position.ticket] = be
|
|
1066
|
+
else:
|
|
1067
|
+
# Skip this if the trail is not set to True
|
|
1068
|
+
if not trail:
|
|
1069
|
+
continue
|
|
1070
|
+
# Check if the price has moved favorably
|
|
1071
|
+
new_be = (
|
|
1072
|
+
round(be * 0.10)
|
|
1073
|
+
if be_plus_points is None
|
|
1074
|
+
else be_plus_points
|
|
1075
|
+
)
|
|
1076
|
+
if trail_after_points is not None:
|
|
1077
|
+
if position.ticket not in self.trail_after_points:
|
|
1078
|
+
# This ensures that the position rich the minimum points required
|
|
1079
|
+
# before the trail can be set
|
|
1080
|
+
new_be = trail_after_points - be
|
|
1081
|
+
self.trail_after_points.append(position.ticket)
|
|
1082
|
+
new_be_points = (
|
|
1083
|
+
self.break_even_points[position.ticket] + new_be
|
|
1084
|
+
)
|
|
1085
|
+
favorable_move = float(points / point) >= new_be_points
|
|
1086
|
+
if favorable_move:
|
|
1087
|
+
# This allows the position to go to take profit in case of a swing trade
|
|
1088
|
+
# If is a scalping position, we can set the stop_trail close to the current price.
|
|
1089
|
+
trail_points = (
|
|
1090
|
+
round(be * 0.50)
|
|
1091
|
+
if stop_trail is None
|
|
1092
|
+
else stop_trail
|
|
1093
|
+
)
|
|
1094
|
+
# Calculate the new break-even level and price
|
|
1095
|
+
if position.type == 0:
|
|
1096
|
+
# This level validate the favorable move of the price
|
|
1097
|
+
new_level = round(
|
|
1098
|
+
position.price_open + (new_be_points * point),
|
|
1099
|
+
digits,
|
|
1100
|
+
)
|
|
1101
|
+
# This price is set away from the current price by the trail_points
|
|
1102
|
+
new_price = round(
|
|
1103
|
+
position.price_current - (trail_points * point),
|
|
1104
|
+
digits,
|
|
1105
|
+
)
|
|
1106
|
+
if new_price < position.sl:
|
|
1107
|
+
new_price = position.sl
|
|
1108
|
+
elif position.type == 1:
|
|
1109
|
+
new_level = round(
|
|
1110
|
+
position.price_open - (new_be_points * point),
|
|
1111
|
+
digits,
|
|
1112
|
+
)
|
|
1113
|
+
new_price = round(
|
|
1114
|
+
position.price_current + (trail_points * point),
|
|
1115
|
+
digits,
|
|
1116
|
+
)
|
|
1117
|
+
if new_price > position.sl:
|
|
1118
|
+
new_price = position.sl
|
|
1119
|
+
self.set_break_even(
|
|
1120
|
+
position, be, price=new_price, level=new_level
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
def set_break_even(
|
|
1124
|
+
self,
|
|
1125
|
+
position: TradePosition,
|
|
1126
|
+
be: int,
|
|
1127
|
+
price: Optional[float] = None,
|
|
1128
|
+
level: Optional[float] = None,
|
|
1129
|
+
):
|
|
1130
|
+
"""
|
|
1131
|
+
Sets the break-even level for a given trading position.
|
|
1132
|
+
|
|
1133
|
+
Args:
|
|
1134
|
+
position (TradePosition): The trading position for which the break-even is to be set. This is the value return by `mt5.positions_get()`.
|
|
1135
|
+
be (int): The break-even level in points.
|
|
1136
|
+
level (float): The break-even level in price, if set to None , it will be calated automaticaly.
|
|
1137
|
+
price (float): The break-even price, if set to None , it will be calated automaticaly.
|
|
1138
|
+
"""
|
|
1139
|
+
point = self.get_symbol_info(self.symbol).point
|
|
1140
|
+
digits = self.get_symbol_info(self.symbol).digits
|
|
1141
|
+
spread = self.get_symbol_info(self.symbol).spread
|
|
1142
|
+
fees = self.get_stats()[0]["average_fee"] * -1
|
|
1143
|
+
risk = self.currency_risk()["trade_profit"]
|
|
1144
|
+
try:
|
|
1145
|
+
fees_points = round((fees / risk), 3)
|
|
1146
|
+
except ZeroDivisionError:
|
|
1147
|
+
fees_points = 0
|
|
1148
|
+
# If Buy
|
|
1149
|
+
if position.type == 0 and position.price_current > position.price_open:
|
|
1150
|
+
# Calculate the break-even level and price
|
|
1151
|
+
break_even_level = position.price_open + (be * point)
|
|
1152
|
+
break_even_price = position.price_open + ((fees_points + spread) * point)
|
|
1153
|
+
# Check if the price specified is greater or lower than the calculated price
|
|
1154
|
+
_price = (
|
|
1155
|
+
break_even_price if price is None or price < break_even_price else price
|
|
1156
|
+
)
|
|
1157
|
+
_level = break_even_level if level is None else level
|
|
1158
|
+
|
|
1159
|
+
if self.get_tick_info(self.symbol).ask > _level:
|
|
1160
|
+
# Set the stop loss to break even
|
|
1161
|
+
request = {
|
|
1162
|
+
"action": Mt5.TRADE_ACTION_SLTP,
|
|
1163
|
+
"position": position.ticket,
|
|
1164
|
+
"sl": round(_price, digits),
|
|
1165
|
+
"tp": position.tp,
|
|
1166
|
+
}
|
|
1167
|
+
self.break_even_request(position.ticket, round(_price, digits), request)
|
|
1168
|
+
# If Sell
|
|
1169
|
+
elif position.type == 1 and position.price_current < position.price_open:
|
|
1170
|
+
break_even_level = position.price_open - (be * point)
|
|
1171
|
+
break_even_price = position.price_open - ((fees_points + spread) * point)
|
|
1172
|
+
_price = (
|
|
1173
|
+
break_even_price if price is None or price > break_even_price else price
|
|
1174
|
+
)
|
|
1175
|
+
_level = break_even_level if level is None else level
|
|
1176
|
+
|
|
1177
|
+
if self.get_tick_info(self.symbol).bid < _level:
|
|
1178
|
+
# Set the stop loss to break even
|
|
1179
|
+
request = {
|
|
1180
|
+
"action": Mt5.TRADE_ACTION_SLTP,
|
|
1181
|
+
"position": position.ticket,
|
|
1182
|
+
"sl": round(_price, digits),
|
|
1183
|
+
"tp": position.tp,
|
|
1184
|
+
}
|
|
1185
|
+
self.break_even_request(position.ticket, round(_price, digits), request)
|
|
1186
|
+
|
|
1187
|
+
def break_even_request(self, tiket, price, request):
|
|
1188
|
+
"""
|
|
1189
|
+
Send a request to set the stop loss to break even for a given trading position.
|
|
1190
|
+
|
|
1191
|
+
Args:
|
|
1192
|
+
tiket (int): The ticket number of the trading position.
|
|
1193
|
+
price (float): The price at which the stop loss is to be set.
|
|
1194
|
+
request (dict): The request to set the stop loss to break even.
|
|
1195
|
+
"""
|
|
1196
|
+
addtionnal = f", SYMBOL={self.symbol}"
|
|
1197
|
+
time.sleep(0.1)
|
|
1198
|
+
try:
|
|
1199
|
+
self.check_order(request)
|
|
1200
|
+
result = self.send_order(request)
|
|
1201
|
+
except Exception as e:
|
|
1202
|
+
print(f"{self.current_datetime()} -", end=" ")
|
|
1203
|
+
trade_retcode_message(
|
|
1204
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1205
|
+
)
|
|
1206
|
+
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1207
|
+
msg = trade_retcode_message(result.retcode)
|
|
1208
|
+
if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
1209
|
+
LOGGER.error(
|
|
1210
|
+
f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
1211
|
+
)
|
|
1212
|
+
tries = 0
|
|
1213
|
+
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
|
|
1214
|
+
if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
1215
|
+
break
|
|
1216
|
+
else:
|
|
1217
|
+
time.sleep(1)
|
|
1218
|
+
try:
|
|
1219
|
+
self.check_order(request)
|
|
1220
|
+
result = self.send_order(request)
|
|
1221
|
+
except Exception as e:
|
|
1222
|
+
print(f"{self.current_datetime()} -", end=" ")
|
|
1223
|
+
trade_retcode_message(
|
|
1224
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1225
|
+
)
|
|
1226
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1227
|
+
break
|
|
1228
|
+
tries += 1
|
|
1229
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1230
|
+
msg = trade_retcode_message(result.retcode)
|
|
1231
|
+
LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
|
|
1232
|
+
info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
|
|
1233
|
+
LOGGER.info(info)
|
|
1234
|
+
self.break_even_status.append(tiket)
|
|
1235
|
+
|
|
1236
|
+
def win_trade(self, position: TradePosition, th: Optional[int] = None) -> bool:
|
|
1237
|
+
"""
|
|
1238
|
+
Check if a positon is wining or looing
|
|
1239
|
+
wen it is closed before be level , tp or sl.
|
|
1240
|
+
|
|
1241
|
+
Args:
|
|
1242
|
+
position (TradePosition): The trading position to check.
|
|
1243
|
+
th (int): The minimum profit for a position in point
|
|
1244
|
+
"""
|
|
1245
|
+
size = self.get_symbol_info(self.symbol).trade_tick_size
|
|
1246
|
+
value = self.get_symbol_info(self.symbol).trade_tick_value
|
|
1247
|
+
points = position.profit * (size / value / position.volume)
|
|
1248
|
+
|
|
1249
|
+
point = self.get_symbol_info(self.symbol).point
|
|
1250
|
+
fees = self.get_stats()[0]["average_fee"] * -1
|
|
1251
|
+
risk = self.currency_risk()["trade_profit"]
|
|
1252
|
+
try:
|
|
1253
|
+
min_be = round((fees / risk)) + 2
|
|
1254
|
+
except ZeroDivisionError:
|
|
1255
|
+
min_be = self.symbol_info.spread
|
|
1256
|
+
be = self.get_break_even()
|
|
1257
|
+
if th is not None:
|
|
1258
|
+
win_be = th
|
|
1259
|
+
else:
|
|
1260
|
+
win_be = max(min_be, round((0.1 * be)))
|
|
1261
|
+
win_trade = float(points / point) >= win_be
|
|
1262
|
+
# Check if the positon is in profit
|
|
1263
|
+
if win_trade:
|
|
1264
|
+
# Check if break-even has already been set for this position
|
|
1265
|
+
if position.ticket not in self.break_even_status:
|
|
1266
|
+
return True
|
|
1267
|
+
return False
|
|
1268
|
+
|
|
1269
|
+
def profit_target(self):
|
|
1270
|
+
fee = 0.0
|
|
1271
|
+
swap = 0.0
|
|
1272
|
+
commission = 0.0
|
|
1273
|
+
profit = 0.0
|
|
1274
|
+
balance = self.get_account_info().balance
|
|
1275
|
+
target = round((balance * self.target) / 100, 2)
|
|
1276
|
+
if len(self.opened_positions) != 0:
|
|
1277
|
+
for position in self.opened_positions:
|
|
1278
|
+
time.sleep(0.1)
|
|
1279
|
+
# This return two TradeDeal Object,
|
|
1280
|
+
# The first one is the opening order
|
|
1281
|
+
# The second is the closing order
|
|
1282
|
+
history = self.get_trades_history(position=position, to_df=False)
|
|
1283
|
+
if history is not None and len(history) == 2:
|
|
1284
|
+
profit += history[1].profit
|
|
1285
|
+
commission += history[0].commission
|
|
1286
|
+
swap += history[0].swap
|
|
1287
|
+
fee += history[0].fee
|
|
1288
|
+
current_profit = profit + commission + fee + swap
|
|
1289
|
+
if current_profit >= target:
|
|
1290
|
+
return True
|
|
1291
|
+
return False
|
|
1292
|
+
|
|
1293
|
+
def close_request(self, request: dict, type: str):
|
|
1294
|
+
"""
|
|
1295
|
+
Close a trading order or position
|
|
1296
|
+
|
|
1297
|
+
Args:
|
|
1298
|
+
request (dict): The request to close a trading order or position
|
|
1299
|
+
type (str): Type of the request ('order', 'position')
|
|
1300
|
+
"""
|
|
1301
|
+
ticket = request[type]
|
|
1302
|
+
addtionnal = f", SYMBOL={self.symbol}"
|
|
1303
|
+
try:
|
|
1304
|
+
self.check_order(request)
|
|
1305
|
+
result = self.send_order(request)
|
|
1306
|
+
except Exception as e:
|
|
1307
|
+
print(f"{self.current_datetime()} -", end=" ")
|
|
1308
|
+
trade_retcode_message(
|
|
1309
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1310
|
+
)
|
|
1311
|
+
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1312
|
+
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
1313
|
+
for fill in FILLING_TYPE:
|
|
1314
|
+
request["type_filling"] = fill
|
|
1315
|
+
result = self.send_order(request)
|
|
1316
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1317
|
+
break
|
|
1318
|
+
elif result.retcode not in self._retcodes:
|
|
1319
|
+
self._retcodes.append(result.retcode)
|
|
1320
|
+
msg = trade_retcode_message(result.retcode)
|
|
1321
|
+
LOGGER.error(
|
|
1322
|
+
f"Closing Order Request, {type.capitalize()}: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
1323
|
+
)
|
|
1324
|
+
else:
|
|
1325
|
+
tries = 0
|
|
1326
|
+
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
1327
|
+
time.sleep(1)
|
|
1328
|
+
try:
|
|
1329
|
+
self.check_order(request)
|
|
1330
|
+
result = self.send_order(request)
|
|
1331
|
+
except Exception as e:
|
|
1332
|
+
print(f"{self.current_datetime()} -", end=" ")
|
|
1333
|
+
trade_retcode_message(
|
|
1334
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1335
|
+
)
|
|
1336
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1337
|
+
break
|
|
1338
|
+
tries += 1
|
|
1339
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1340
|
+
msg = trade_retcode_message(result.retcode)
|
|
1341
|
+
LOGGER.info(f"Closing Order {msg}{addtionnal}")
|
|
1342
|
+
info = f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol}, Price: @{request.get('price', 0.0)}"
|
|
1343
|
+
LOGGER.info(info)
|
|
1344
|
+
return True
|
|
1345
|
+
else:
|
|
1346
|
+
return False
|
|
1347
|
+
|
|
1348
|
+
def modify_order(
|
|
1349
|
+
self,
|
|
1350
|
+
ticket: int,
|
|
1351
|
+
price: Optional[float] = None,
|
|
1352
|
+
stoplimit: Optional[float] = None,
|
|
1353
|
+
sl: Optional[float] = None,
|
|
1354
|
+
tp: Optional[float] = None,
|
|
1355
|
+
):
|
|
1356
|
+
"""
|
|
1357
|
+
Modify an open order by it ticket
|
|
1358
|
+
|
|
1359
|
+
Args:
|
|
1360
|
+
ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
|
|
1361
|
+
price (float): The price at which to modify the order
|
|
1362
|
+
stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
|
|
1363
|
+
The pending order is not passed to the trading system until that moment
|
|
1364
|
+
sl (float): The stop loss in points
|
|
1365
|
+
tp (float): The take profit in points
|
|
1366
|
+
"""
|
|
1367
|
+
orders = self.get_orders(ticket=ticket) or []
|
|
1368
|
+
if len(orders) == 0:
|
|
1369
|
+
LOGGER.error(
|
|
1370
|
+
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={price}"
|
|
1371
|
+
)
|
|
1372
|
+
return
|
|
1373
|
+
order = orders[0]
|
|
1374
|
+
request = {
|
|
1375
|
+
"action": Mt5.TRADE_ACTION_MODIFY,
|
|
1376
|
+
"order": ticket,
|
|
1377
|
+
"price": price or order.price_open,
|
|
1378
|
+
"sl": sl or order.sl,
|
|
1379
|
+
"tp": tp or order.tp,
|
|
1380
|
+
"stoplimit": stoplimit or order.price_stoplimit,
|
|
1381
|
+
}
|
|
1382
|
+
self.check_order(request)
|
|
1383
|
+
result = self.send_order(request)
|
|
1384
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1385
|
+
LOGGER.info(
|
|
1386
|
+
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={price}, SL={sl}, TP={tp}, STOP_LIMIT={stoplimit}"
|
|
1387
|
+
)
|
|
1388
|
+
else:
|
|
1389
|
+
msg = trade_retcode_message(result.retcode)
|
|
1390
|
+
LOGGER.error(
|
|
1391
|
+
f"Unable to modify Order #{ticket}, RETCODE={result.retcode}: {msg}, SYMBOL={self.symbol}"
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
def close_order(
|
|
1395
|
+
self, ticket: int, id: Optional[int] = None, comment: Optional[str] = None
|
|
1396
|
+
):
|
|
1397
|
+
"""
|
|
1398
|
+
Close an open order by it ticket
|
|
1399
|
+
|
|
1400
|
+
Args:
|
|
1401
|
+
ticket (int): Order ticket to close (e.g TradeOrder.ticket)
|
|
1402
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1403
|
+
comment (str): Comment for the closing position
|
|
1404
|
+
|
|
1405
|
+
Returns:
|
|
1406
|
+
- True if order closed, False otherwise
|
|
1407
|
+
"""
|
|
1408
|
+
request = {
|
|
1409
|
+
"action": Mt5.TRADE_ACTION_REMOVE,
|
|
1410
|
+
"symbol": self.symbol,
|
|
1411
|
+
"order": ticket,
|
|
1412
|
+
"magic": id if id is not None else self.expert_id,
|
|
1413
|
+
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
1414
|
+
}
|
|
1415
|
+
return self.close_request(request, type="order")
|
|
1416
|
+
|
|
1417
|
+
def close_position(
|
|
1418
|
+
self,
|
|
1419
|
+
ticket: int,
|
|
1420
|
+
id: Optional[int] = None,
|
|
1421
|
+
pct: Optional[float] = 1.0,
|
|
1422
|
+
comment: Optional[str] = None,
|
|
1423
|
+
symbol: Optional[str] = None,
|
|
1424
|
+
) -> bool:
|
|
1425
|
+
"""
|
|
1426
|
+
Close an open position by it ticket
|
|
1427
|
+
|
|
1428
|
+
Args:
|
|
1429
|
+
ticket (int): Positon ticket to close (e.g TradePosition.ticket)
|
|
1430
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1431
|
+
pct (float): Percentage of the position to close
|
|
1432
|
+
comment (str): Comment for the closing position
|
|
1433
|
+
|
|
1434
|
+
Returns:
|
|
1435
|
+
- True if position closed, False otherwise
|
|
1436
|
+
"""
|
|
1437
|
+
# get all Actives positions
|
|
1438
|
+
time.sleep(0.1)
|
|
1439
|
+
symbol = symbol or self.symbol
|
|
1440
|
+
Id = id if id is not None else self.expert_id
|
|
1441
|
+
positions = self.get_positions(ticket=ticket)
|
|
1442
|
+
buy_price = self.get_tick_info(symbol).ask
|
|
1443
|
+
sell_price = self.get_tick_info(symbol).bid
|
|
1444
|
+
deviation = self.get_deviation()
|
|
1445
|
+
if positions is not None and len(positions) == 1:
|
|
1446
|
+
position = positions[0]
|
|
1447
|
+
if position.ticket == ticket and position.magic == Id:
|
|
1448
|
+
buy = position.type == 0
|
|
1449
|
+
request = {
|
|
1450
|
+
"action": Mt5.TRADE_ACTION_DEAL,
|
|
1451
|
+
"symbol": symbol,
|
|
1452
|
+
"volume": (position.volume * pct),
|
|
1453
|
+
"type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
|
|
1454
|
+
"position": ticket,
|
|
1455
|
+
"price": sell_price if buy else buy_price,
|
|
1456
|
+
"deviation": deviation,
|
|
1457
|
+
"magic": Id,
|
|
1458
|
+
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
1459
|
+
"type_time": Mt5.ORDER_TIME_GTC,
|
|
1460
|
+
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
1461
|
+
}
|
|
1462
|
+
return self.close_request(request, type="position")
|
|
1463
|
+
|
|
1464
|
+
def bulk_close(
|
|
1465
|
+
self,
|
|
1466
|
+
tickets: List,
|
|
1467
|
+
tikets_type: Literal["positions", "orders"],
|
|
1468
|
+
close_func: Callable,
|
|
1469
|
+
order_type: str,
|
|
1470
|
+
id: Optional[int] = None,
|
|
1471
|
+
comment: Optional[str] = None,
|
|
1472
|
+
):
|
|
1473
|
+
"""
|
|
1474
|
+
Close multiple orders or positions at once.
|
|
1475
|
+
|
|
1476
|
+
Args:
|
|
1477
|
+
tickets (List): List of tickets to close
|
|
1478
|
+
tikets_type (str): Type of tickets to close ('positions', 'orders')
|
|
1479
|
+
close_func (Callable): The function to close the tickets
|
|
1480
|
+
order_type (str): Type of orders or positions to close
|
|
1481
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1482
|
+
comment (str): Comment for the closing position
|
|
1483
|
+
"""
|
|
1484
|
+
if order_type == "all":
|
|
1485
|
+
order_type = "open"
|
|
1486
|
+
if tickets is not None and len(tickets) > 0:
|
|
1487
|
+
for ticket in tickets.copy():
|
|
1488
|
+
if close_func(ticket, id=id, comment=comment):
|
|
1489
|
+
tickets.remove(ticket)
|
|
1490
|
+
time.sleep(1)
|
|
1491
|
+
if tickets is not None and len(tickets) == 0:
|
|
1492
|
+
LOGGER.info(
|
|
1493
|
+
f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}."
|
|
1494
|
+
)
|
|
1495
|
+
else:
|
|
1496
|
+
LOGGER.info(
|
|
1497
|
+
f"{len(tickets)} {order_type.upper()} {tikets_type.upper()} not closed, SYMBOL={self.symbol}"
|
|
1498
|
+
)
|
|
1499
|
+
else:
|
|
1500
|
+
LOGGER.info(
|
|
1501
|
+
f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}."
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1504
|
+
Orders = Literal[
|
|
1505
|
+
"all",
|
|
1506
|
+
"buy_stops",
|
|
1507
|
+
"sell_stops",
|
|
1508
|
+
"buy_limits",
|
|
1509
|
+
"sell_limits",
|
|
1510
|
+
"buy_stop_limits",
|
|
1511
|
+
"sell_stop_limits",
|
|
1512
|
+
]
|
|
1513
|
+
|
|
1514
|
+
def close_orders(
|
|
1515
|
+
self,
|
|
1516
|
+
order_type: Orders,
|
|
1517
|
+
id: Optional[int] = None,
|
|
1518
|
+
comment: Optional[str] = None,
|
|
1519
|
+
):
|
|
1520
|
+
"""
|
|
1521
|
+
Args:
|
|
1522
|
+
order_type (str): Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
|
|
1523
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1524
|
+
comment (str): Comment for the closing position
|
|
1525
|
+
"""
|
|
1526
|
+
id = id if id is not None else self.expert_id
|
|
1527
|
+
if order_type == "all":
|
|
1528
|
+
orders = self.get_current_orders(id=id)
|
|
1529
|
+
elif order_type == "buy_stops":
|
|
1530
|
+
orders = self.get_current_buy_stops(id=id)
|
|
1531
|
+
elif order_type == "sell_stops":
|
|
1532
|
+
orders = self.get_current_sell_stops(id=id)
|
|
1533
|
+
elif order_type == "buy_limits":
|
|
1534
|
+
orders = self.get_current_buy_limits(id=id)
|
|
1535
|
+
elif order_type == "sell_limits":
|
|
1536
|
+
orders = self.get_current_sell_limits(id=id)
|
|
1537
|
+
elif order_type == "buy_stop_limits":
|
|
1538
|
+
orders = self.get_current_buy_stop_limits(id=id)
|
|
1539
|
+
elif order_type == "sell_stop_limits":
|
|
1540
|
+
orders = self.get_current_sell_stop_limits(id=id)
|
|
1541
|
+
else:
|
|
1542
|
+
LOGGER.error(f"Invalid order type: {order_type}")
|
|
1543
|
+
return
|
|
1544
|
+
self.bulk_close(
|
|
1545
|
+
orders, "orders", self.close_order, order_type, id=id, comment=comment
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
1549
|
+
|
|
1550
|
+
def close_positions(
|
|
1551
|
+
self,
|
|
1552
|
+
position_type: Positions,
|
|
1553
|
+
id: Optional[int] = None,
|
|
1554
|
+
comment: Optional[str] = None,
|
|
1555
|
+
):
|
|
1556
|
+
"""
|
|
1557
|
+
Args:
|
|
1558
|
+
position_type (str): Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')
|
|
1559
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1560
|
+
comment (str): Comment for the closing position
|
|
1561
|
+
"""
|
|
1562
|
+
id = id if id is not None else self.expert_id
|
|
1563
|
+
if position_type == "all":
|
|
1564
|
+
positions = self.get_current_positions(id=id)
|
|
1565
|
+
elif position_type == "buy":
|
|
1566
|
+
positions = self.get_current_buys(id=id)
|
|
1567
|
+
elif position_type == "sell":
|
|
1568
|
+
positions = self.get_current_sells(id=id)
|
|
1569
|
+
elif position_type == "profitable":
|
|
1570
|
+
positions = self.get_current_profitables(id=id)
|
|
1571
|
+
elif position_type == "losing":
|
|
1572
|
+
positions = self.get_current_losings(id=id)
|
|
1573
|
+
else:
|
|
1574
|
+
LOGGER.error(f"Invalid position type: {position_type}")
|
|
1575
|
+
return
|
|
1576
|
+
self.bulk_close(
|
|
1577
|
+
positions,
|
|
1578
|
+
"positions",
|
|
1579
|
+
self.close_position,
|
|
1580
|
+
position_type,
|
|
1581
|
+
id=id,
|
|
1582
|
+
comment=comment,
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
def get_stats(self) -> Tuple[Dict[str, Any]]:
|
|
1586
|
+
"""
|
|
1587
|
+
get some stats about the trading day and trading history
|
|
1588
|
+
|
|
1589
|
+
:return: tuple[Dict[str, Any]]
|
|
1590
|
+
"""
|
|
1591
|
+
# get history of deals for one trading session
|
|
1592
|
+
profit = 0.0
|
|
1593
|
+
total_fees = 0.0
|
|
1594
|
+
loss_trades = 0
|
|
1595
|
+
win_trades = 0
|
|
1596
|
+
balance = self.get_account_info().balance
|
|
1597
|
+
deals = len(self.opened_positions)
|
|
1598
|
+
if deals != 0:
|
|
1599
|
+
for position in self.opened_positions:
|
|
1600
|
+
time.sleep(0.1)
|
|
1601
|
+
history = self.get_trades_history(position=position, to_df=False)
|
|
1602
|
+
if history is not None and len(history) == 2:
|
|
1603
|
+
result = history[1].profit
|
|
1604
|
+
comm = history[0].commission
|
|
1605
|
+
swap = history[0].swap
|
|
1606
|
+
fee = history[0].fee
|
|
1607
|
+
if (result + comm + swap + fee) <= 0:
|
|
1608
|
+
loss_trades += 1
|
|
1609
|
+
else:
|
|
1610
|
+
win_trades += 1
|
|
1611
|
+
profit += result
|
|
1612
|
+
total_fees += comm + swap + fee
|
|
1613
|
+
average_fee = total_fees / deals
|
|
1614
|
+
win_rate = round((win_trades / deals) * 100, 2)
|
|
1615
|
+
stats1 = {
|
|
1616
|
+
"deals": deals,
|
|
1617
|
+
"profit": profit,
|
|
1618
|
+
"win_trades": win_trades,
|
|
1619
|
+
"loss_trades": loss_trades,
|
|
1620
|
+
"total_fees": total_fees,
|
|
1621
|
+
"average_fee": average_fee,
|
|
1622
|
+
"win_rate": win_rate,
|
|
1623
|
+
}
|
|
1624
|
+
else:
|
|
1625
|
+
stats1 = {
|
|
1626
|
+
"deals": 0,
|
|
1627
|
+
"profit": 0,
|
|
1628
|
+
"win_trades": 0,
|
|
1629
|
+
"loss_trades": 0,
|
|
1630
|
+
"total_fees": 0,
|
|
1631
|
+
"average_fee": 0,
|
|
1632
|
+
"win_rate": 0,
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
# Get total stats
|
|
1636
|
+
df = self.get_trades_history()
|
|
1637
|
+
if df is not None:
|
|
1638
|
+
df2 = df.iloc[1:]
|
|
1639
|
+
profit = df2["profit"].sum()
|
|
1640
|
+
commisions = df2["commission"].sum()
|
|
1641
|
+
_fees = df2["fee"].sum()
|
|
1642
|
+
_swap = df2["swap"].sum()
|
|
1643
|
+
total_profit = commisions + _fees + _swap + profit
|
|
1644
|
+
account_info = self.get_account_info()
|
|
1645
|
+
balance = account_info.balance
|
|
1646
|
+
initial_balance = balance - total_profit
|
|
1647
|
+
profittable = "Yes" if balance > initial_balance else "No"
|
|
1648
|
+
stats2 = {"total_profit": total_profit, "profitability": profittable}
|
|
1649
|
+
else:
|
|
1650
|
+
stats2 = {"total_profit": 0, "profitability": 0}
|
|
1651
|
+
return (stats1, stats2)
|
|
1652
|
+
|
|
1653
|
+
def sharpe(self):
|
|
1654
|
+
"""
|
|
1655
|
+
Calculate the Sharpe ratio of a returns stream
|
|
1656
|
+
based on a number of trading periods.
|
|
1657
|
+
The function assumes that the returns are the excess of
|
|
1658
|
+
those compared to a benchmark.
|
|
1659
|
+
"""
|
|
1660
|
+
# Get total history
|
|
1661
|
+
df2 = self.get_trades_history()
|
|
1662
|
+
if df2 is None:
|
|
1663
|
+
return 0.0
|
|
1664
|
+
df = df2.iloc[1:]
|
|
1665
|
+
profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
|
|
1666
|
+
returns = profit.pct_change(fill_method=None)
|
|
1667
|
+
periods = self.max_trade() * 252
|
|
1668
|
+
sharpe = create_sharpe_ratio(returns, periods=periods)
|
|
1669
|
+
|
|
1670
|
+
return round(sharpe, 3)
|
|
1671
|
+
|
|
1672
|
+
def days_end(self) -> bool:
|
|
1673
|
+
"""Check if it is the end of the trading day."""
|
|
1674
|
+
current_hour = datetime.now().hour
|
|
1675
|
+
current_minute = datetime.now().minute
|
|
1676
|
+
|
|
1677
|
+
ending_hour = int(self.ending_time_hour)
|
|
1678
|
+
ending_minute = int(self.ending_time_minutes)
|
|
1679
|
+
|
|
1680
|
+
if current_hour > ending_hour or (
|
|
1681
|
+
current_hour == ending_hour and current_minute >= ending_minute
|
|
1682
|
+
):
|
|
1683
|
+
return True
|
|
1684
|
+
else:
|
|
1685
|
+
return False
|
|
1686
|
+
|
|
1687
|
+
def trading_time(self):
|
|
1688
|
+
"""Check if it is time to trade."""
|
|
1689
|
+
if (
|
|
1690
|
+
int(self.start_time_hour)
|
|
1691
|
+
< datetime.now().hour
|
|
1692
|
+
< int(self.finishing_time_hour)
|
|
1693
|
+
):
|
|
1694
|
+
return True
|
|
1695
|
+
elif datetime.now().hour == int(self.start_time_hour):
|
|
1696
|
+
if datetime.now().minute >= int(self.start_time_minutes):
|
|
1697
|
+
return True
|
|
1698
|
+
elif datetime.now().hour == int(self.finishing_time_hour):
|
|
1699
|
+
if datetime.now().minute < int(self.finishing_time_minutes):
|
|
1700
|
+
return True
|
|
1701
|
+
return False
|
|
1702
|
+
|
|
1703
|
+
def sleep_time(self, weekend=False):
|
|
1704
|
+
if weekend:
|
|
1705
|
+
# claculate number of minute from the friday and to monday start
|
|
1706
|
+
friday_time = datetime.strptime(self.current_time(), "%H:%M")
|
|
1707
|
+
monday_time = datetime.strptime(self.start, "%H:%M")
|
|
1708
|
+
intra_day_diff = (monday_time - friday_time).total_seconds() // 60
|
|
1709
|
+
inter_day_diff = 3 * 24 * 60
|
|
1710
|
+
total_minutes = inter_day_diff + intra_day_diff
|
|
1711
|
+
return total_minutes
|
|
1712
|
+
else:
|
|
1713
|
+
# claculate number of minute from the end to the start
|
|
1714
|
+
start = datetime.strptime(self.start, "%H:%M")
|
|
1715
|
+
end = datetime.strptime(self.current_time(), "%H:%M")
|
|
1716
|
+
minutes = (end - start).total_seconds() // 60
|
|
1717
|
+
sleep_time = (24 * 60) - minutes
|
|
1718
|
+
return sleep_time
|
|
1719
|
+
|
|
1720
|
+
def current_datetime(self):
|
|
1721
|
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1722
|
+
|
|
1723
|
+
def current_time(self, seconds=False):
|
|
1724
|
+
if seconds:
|
|
1725
|
+
return datetime.now().strftime("%H:%M:%S")
|
|
1726
|
+
return datetime.now().strftime("%H:%M")
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def create_trade_instance(
|
|
1730
|
+
symbols: List[str],
|
|
1731
|
+
params: Dict[str, Any],
|
|
1732
|
+
daily_risk: Optional[Dict[str, float]] = None,
|
|
1733
|
+
max_risk: Optional[Dict[str, float]] = None,
|
|
1734
|
+
pchange_sl: Optional[Dict[str, float] | float] = None,
|
|
1735
|
+
**kwargs,
|
|
1736
|
+
) -> Dict[str, Trade]:
|
|
1737
|
+
"""
|
|
1738
|
+
Creates Trade instances for each symbol provided.
|
|
1739
|
+
|
|
1740
|
+
Args:
|
|
1741
|
+
symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
|
|
1742
|
+
params: A dictionary containing parameters for the Trade instance.
|
|
1743
|
+
daily_risk: A dictionary containing daily risk weight for each symbol.
|
|
1744
|
+
max_risk: A dictionary containing maximum risk weight for each symbol.
|
|
1745
|
+
|
|
1746
|
+
Returns:
|
|
1747
|
+
A dictionary where keys are symbols and values are corresponding Trade instances.
|
|
1748
|
+
|
|
1749
|
+
Raises:
|
|
1750
|
+
ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
|
|
1751
|
+
|
|
1752
|
+
Note:
|
|
1753
|
+
`daily_risk` and `max_risk` can be used to manage the risk of each symbol
|
|
1754
|
+
based on the importance of the symbol in the portfolio or strategy.
|
|
1755
|
+
See bbstrader.metatrader.trade.Trade for more details.
|
|
1756
|
+
"""
|
|
1757
|
+
if not isinstance(params.get("logger"), (Logger, type(log))):
|
|
1758
|
+
loggr = log
|
|
1759
|
+
else:
|
|
1760
|
+
loggr = params.get("logger")
|
|
1761
|
+
ids = params.get("expert_id", None)
|
|
1762
|
+
trade_instances = {}
|
|
1763
|
+
if not symbols:
|
|
1764
|
+
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1765
|
+
if not params:
|
|
1766
|
+
raise ValueError("The 'params' dictionary cannot be empty.")
|
|
1767
|
+
|
|
1768
|
+
if daily_risk is not None:
|
|
1769
|
+
for symbol in symbols:
|
|
1770
|
+
if symbol not in daily_risk:
|
|
1771
|
+
raise ValueError(f"Missing daily risk weight for symbol '{symbol}'.")
|
|
1772
|
+
if max_risk is not None:
|
|
1773
|
+
for symbol in symbols:
|
|
1774
|
+
if symbol not in max_risk:
|
|
1775
|
+
raise ValueError(
|
|
1776
|
+
f"Missing maximum risk percentage for symbol '{symbol}'."
|
|
1777
|
+
)
|
|
1778
|
+
if pchange_sl is not None:
|
|
1779
|
+
if isinstance(pchange_sl, dict):
|
|
1780
|
+
for symbol in symbols:
|
|
1781
|
+
if symbol not in pchange_sl:
|
|
1782
|
+
raise ValueError(
|
|
1783
|
+
f"Missing percentage change for symbol '{symbol}'."
|
|
1784
|
+
)
|
|
1785
|
+
if isinstance(ids, dict):
|
|
1786
|
+
for symbol in symbols:
|
|
1787
|
+
if symbol not in ids:
|
|
1788
|
+
raise ValueError(f"Missing expert ID for symbol '{symbol}'.")
|
|
1789
|
+
|
|
1790
|
+
for symbol in symbols:
|
|
1791
|
+
try:
|
|
1792
|
+
params["symbol"] = symbol
|
|
1793
|
+
params["expert_id"] = (
|
|
1794
|
+
ids[symbol]
|
|
1795
|
+
if ids is not None and isinstance(ids, dict)
|
|
1796
|
+
else ids
|
|
1797
|
+
if ids is not None and isinstance(ids, (int, float))
|
|
1798
|
+
else params["expert_id"]
|
|
1799
|
+
if "expert_id" in params
|
|
1800
|
+
else None
|
|
1801
|
+
)
|
|
1802
|
+
params["pchange_sl"] = (
|
|
1803
|
+
pchange_sl[symbol]
|
|
1804
|
+
if pchange_sl is not None and isinstance(pchange_sl, dict)
|
|
1805
|
+
else pchange_sl
|
|
1806
|
+
if pchange_sl is not None and isinstance(pchange_sl, (int, float))
|
|
1807
|
+
else params["pchange_sl"]
|
|
1808
|
+
if "pchange_sl" in params
|
|
1809
|
+
else None
|
|
1810
|
+
)
|
|
1811
|
+
params["daily_risk"] = (
|
|
1812
|
+
daily_risk[symbol]
|
|
1813
|
+
if daily_risk is not None
|
|
1814
|
+
else params["daily_risk"]
|
|
1815
|
+
if "daily_risk" in params
|
|
1816
|
+
else None
|
|
1817
|
+
)
|
|
1818
|
+
params["max_risk"] = (
|
|
1819
|
+
max_risk[symbol]
|
|
1820
|
+
if max_risk is not None
|
|
1821
|
+
else params["max_risk"]
|
|
1822
|
+
if "max_risk" in params
|
|
1823
|
+
else 10.0
|
|
1824
|
+
)
|
|
1825
|
+
trade_instances[symbol] = Trade(**params)
|
|
1826
|
+
except Exception as e:
|
|
1827
|
+
loggr.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1828
|
+
|
|
1829
|
+
if len(trade_instances) != len(symbols):
|
|
1830
|
+
for symbol in symbols:
|
|
1831
|
+
if symbol not in trade_instances:
|
|
1832
|
+
loggr.error(f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1833
|
+
loggr.info(
|
|
1834
|
+
f"Trade instances created successfully for {len(trade_instances)} symbols."
|
|
1835
|
+
)
|
|
1836
|
+
return trade_instances
|