bbstrader 0.0.1__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 +17 -0
- bbstrader/btengine/__init__.py +50 -0
- bbstrader/btengine/backtest.py +900 -0
- bbstrader/btengine/data.py +374 -0
- bbstrader/btengine/event.py +201 -0
- bbstrader/btengine/execution.py +83 -0
- bbstrader/btengine/performance.py +309 -0
- bbstrader/btengine/portfolio.py +326 -0
- bbstrader/btengine/strategy.py +31 -0
- bbstrader/metatrader/__init__.py +6 -0
- bbstrader/metatrader/account.py +1038 -0
- bbstrader/metatrader/rates.py +226 -0
- bbstrader/metatrader/risk.py +626 -0
- bbstrader/metatrader/trade.py +1296 -0
- bbstrader/metatrader/utils.py +669 -0
- bbstrader/models/__init__.py +6 -0
- bbstrader/models/risk.py +349 -0
- bbstrader/strategies.py +681 -0
- bbstrader/trading/__init__.py +4 -0
- bbstrader/trading/execution.py +965 -0
- bbstrader/trading/run.py +131 -0
- bbstrader/trading/utils.py +153 -0
- bbstrader/tseries.py +592 -0
- bbstrader-0.0.1.dist-info/LICENSE +21 -0
- bbstrader-0.0.1.dist-info/METADATA +132 -0
- bbstrader-0.0.1.dist-info/RECORD +28 -0
- bbstrader-0.0.1.dist-info/WHEEL +5 -0
- bbstrader-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1296 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import csv
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
import numpy as np
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import MetaTrader5 as Mt5
|
|
8
|
+
from typing import List, Tuple, Dict, Any, Optional, Literal
|
|
9
|
+
from bbstrader.metatrader.risk import RiskManagement
|
|
10
|
+
from bbstrader.metatrader.account import INIT_MSG
|
|
11
|
+
from bbstrader.metatrader.utils import (
|
|
12
|
+
TimeFrame, TradePosition, TickInfo,
|
|
13
|
+
raise_mt5_error, trade_retcode_message, config_logger
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Configure the logger
|
|
17
|
+
logger = config_logger('trade.log', console_log=True)
|
|
18
|
+
|
|
19
|
+
class Trade(RiskManagement):
|
|
20
|
+
"""
|
|
21
|
+
Extends the `RiskManagement` class to include specific trading operations,
|
|
22
|
+
incorporating risk management strategies directly into trade executions.
|
|
23
|
+
It offers functionalities to execute trades while managing risks
|
|
24
|
+
according to the inherited RiskManagement parameters and methods.
|
|
25
|
+
|
|
26
|
+
Exemple:
|
|
27
|
+
>>> import time
|
|
28
|
+
>>> # Initialize the Trade class with parameters
|
|
29
|
+
>>> trade = Trade(
|
|
30
|
+
... symbol="#AAPL", # Symbol to trade
|
|
31
|
+
... expert_name="MyExpertAdvisor",# Name of the expert advisor
|
|
32
|
+
... expert_id=12345, # Unique ID for the expert advisor
|
|
33
|
+
... version="1.0", # Version of the expert advisor
|
|
34
|
+
... target=5.0, # Daily profit target in percentage
|
|
35
|
+
... start_time="09:00", # Start time for trading
|
|
36
|
+
... finishing_time="17:00", # Time to stop opening new positions
|
|
37
|
+
... ending_time="17:30", # Time to close any open positions
|
|
38
|
+
... max_risk=2.0, # Maximum risk allowed on the account in percentage
|
|
39
|
+
... daily_risk=1.0, # Daily risk allowed in percentage
|
|
40
|
+
... max_trades=5, # Maximum number of trades per session
|
|
41
|
+
... rr=2.0, # Risk-reward ratio
|
|
42
|
+
... account_leverage=True, # Use account leverage in calculations
|
|
43
|
+
... std_stop=True, # Use standard deviation for stop loss calculation
|
|
44
|
+
... sl=20, # Stop loss in points (optional)
|
|
45
|
+
... tp=30, # Take profit in points (optional)
|
|
46
|
+
... be=10 # Break-even in points (optional)
|
|
47
|
+
... )
|
|
48
|
+
|
|
49
|
+
>>> # Example to open a buy position
|
|
50
|
+
>>> trade.open_buy_position(mm=True, comment="Opening Buy Position")
|
|
51
|
+
|
|
52
|
+
>>> # Example to open a sell position
|
|
53
|
+
>>> trade.open_sell_position(mm=True, comment="Opening Sell Position")
|
|
54
|
+
|
|
55
|
+
>>> # Check current open positions
|
|
56
|
+
>>> opened_positions = trade.get_opened_positions
|
|
57
|
+
>>> if opened_positions is not None:
|
|
58
|
+
... print(f"Current open positions: {opened_positions}")
|
|
59
|
+
|
|
60
|
+
>>> # Close all open positions at the end of the trading session
|
|
61
|
+
>>> if trade.days_end():
|
|
62
|
+
... trade.close_all_positions(comment="Closing all positions at day's end")
|
|
63
|
+
|
|
64
|
+
>>> # Print trading session statistics
|
|
65
|
+
>>> trade.statistics(save=True, dir="my_trading_stats")
|
|
66
|
+
|
|
67
|
+
>>> # Sleep until the next trading session if needed (example usage)
|
|
68
|
+
>>> sleep_time = trade.sleep_time()
|
|
69
|
+
>>> print(f"Sleeping for {sleep_time} minutes until the next trading session.")
|
|
70
|
+
>>> time.sleep(sleep_time * 60)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
symbol: str = 'EURUSD',
|
|
76
|
+
expert_name: str = 'bbstrader',
|
|
77
|
+
expert_id: int = 9818,
|
|
78
|
+
version: str = '1.0',
|
|
79
|
+
target: float = 5.0,
|
|
80
|
+
start_time: str = "1:00",
|
|
81
|
+
finishing_time: str = "23:00",
|
|
82
|
+
ending_time: str = "23:30",
|
|
83
|
+
verbose: Optional[bool] = None,
|
|
84
|
+
**kwargs,
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Initializes the Trade class with the specified parameters.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
symbol (str): The `symbol` that the expert advisor will trade.
|
|
91
|
+
expert_name (str): The name of the `expert advisor`.
|
|
92
|
+
expert_id (int): The `unique ID` used to identify the expert advisor
|
|
93
|
+
or the strategy used on the symbol.
|
|
94
|
+
version (str): The `version` of the expert advisor.
|
|
95
|
+
target (float): `Trading period (day, week, month) profit target` in percentage
|
|
96
|
+
start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
|
|
97
|
+
finishing_time (str): The time after which no new position can be opened.
|
|
98
|
+
ending_time (str): The time after which any open position will be closed.
|
|
99
|
+
verbose (bool | None): If set to None (default), account summary and risk managment
|
|
100
|
+
parameters are printed in the terminal.
|
|
101
|
+
|
|
102
|
+
Inherits:
|
|
103
|
+
- max_risk
|
|
104
|
+
- max_trades
|
|
105
|
+
- rr
|
|
106
|
+
- daily_risk
|
|
107
|
+
- time_frame
|
|
108
|
+
- account_leverage
|
|
109
|
+
- std_stop
|
|
110
|
+
- pchange_sl
|
|
111
|
+
- sl
|
|
112
|
+
- tp
|
|
113
|
+
- be
|
|
114
|
+
See the RiskManagement class for more details on these parameters.
|
|
115
|
+
"""
|
|
116
|
+
# Call the parent class constructor first
|
|
117
|
+
super().__init__(
|
|
118
|
+
symbol=symbol,
|
|
119
|
+
start_time=start_time,
|
|
120
|
+
finishing_time=finishing_time,
|
|
121
|
+
**kwargs, # Pass kwargs to the parent constructor
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Initialize Trade-specific attributes
|
|
125
|
+
self.symbol = symbol
|
|
126
|
+
self.expert_name = expert_name
|
|
127
|
+
self.expert_id = expert_id
|
|
128
|
+
self.version = version
|
|
129
|
+
self.target = target
|
|
130
|
+
self.verbose = verbose
|
|
131
|
+
self.start = start_time
|
|
132
|
+
self.end = ending_time
|
|
133
|
+
self.finishing = finishing_time
|
|
134
|
+
self.tf = kwargs.get("time_frame", 'D1')
|
|
135
|
+
|
|
136
|
+
self.lot = self.get_lot()
|
|
137
|
+
self.stop_loss = self.get_stop_loss()
|
|
138
|
+
self.take_profit = self.get_take_profit()
|
|
139
|
+
self.break_even_points = self.get_break_even()
|
|
140
|
+
self.deviation = self.get_deviation()
|
|
141
|
+
|
|
142
|
+
self.start_time_hour, self.start_time_minutes = self.start.split(":")
|
|
143
|
+
self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
|
|
144
|
+
":")
|
|
145
|
+
self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
|
|
146
|
+
|
|
147
|
+
self.buy_positions = []
|
|
148
|
+
self.sell_positions = []
|
|
149
|
+
self.opened_positions = []
|
|
150
|
+
self.opened_orders = []
|
|
151
|
+
self.break_even_status = []
|
|
152
|
+
|
|
153
|
+
self.initialize()
|
|
154
|
+
self.select_symbol()
|
|
155
|
+
self.prepare_symbol()
|
|
156
|
+
|
|
157
|
+
if self.verbose:
|
|
158
|
+
self.summary()
|
|
159
|
+
time.sleep(1)
|
|
160
|
+
print()
|
|
161
|
+
self.risk_managment()
|
|
162
|
+
print(
|
|
163
|
+
f">>> Everything is OK, @{self.expert_name} is Running ....>>>\n")
|
|
164
|
+
|
|
165
|
+
def initialize(self):
|
|
166
|
+
"""
|
|
167
|
+
Initializes the MetaTrader 5 (MT5) terminal for trading operations.
|
|
168
|
+
This method attempts to establish a connection with the MT5 terminal.
|
|
169
|
+
If the initial connection attempt fails due to a timeout, it retries after a specified delay.
|
|
170
|
+
Successful initialization is crucial for the execution of trading operations.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
MT5TerminalError: If initialization fails.
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
if self.verbose:
|
|
177
|
+
print("\nInitializing the basics.")
|
|
178
|
+
if not Mt5.initialize():
|
|
179
|
+
raise_mt5_error(message=INIT_MSG)
|
|
180
|
+
if self.verbose:
|
|
181
|
+
print(
|
|
182
|
+
f"You are running the @{self.expert_name} Expert advisor,"
|
|
183
|
+
f" Version @{self.version}, on {self.symbol}."
|
|
184
|
+
)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"During initialization: {e}")
|
|
187
|
+
|
|
188
|
+
def select_symbol(self):
|
|
189
|
+
"""
|
|
190
|
+
Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
|
|
191
|
+
This method ensures that the specified trading
|
|
192
|
+
symbol is selected and visible in the MT5 terminal,
|
|
193
|
+
allowing subsequent trading operations such as opening and
|
|
194
|
+
closing positions on this symbol.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
MT5TerminalError: If symbole selection fails.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
if not Mt5.symbol_select(self.symbol, True):
|
|
201
|
+
raise_mt5_error(message=INIT_MSG)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"Selecting symbol '{self.symbol}': {e}")
|
|
204
|
+
|
|
205
|
+
def prepare_symbol(self):
|
|
206
|
+
"""
|
|
207
|
+
Prepares the selected symbol for trading.
|
|
208
|
+
This method checks if the symbol is available and visible in the
|
|
209
|
+
MT5 terminal. If the symbol is not visible, it attempts to select the symbol again.
|
|
210
|
+
This step ensures that trading operations can be performed on the selected symbol without issues.
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
MT5TerminalError: If the symbol cannot be made visible for trading operations.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
symbol_info = self.get_symbol_info(self.symbol)
|
|
217
|
+
if symbol_info is None:
|
|
218
|
+
raise_mt5_error(message=INIT_MSG)
|
|
219
|
+
|
|
220
|
+
if not symbol_info.visible:
|
|
221
|
+
raise_mt5_error(message=INIT_MSG)
|
|
222
|
+
if self.verbose:
|
|
223
|
+
print("Initialization successfully completed.")
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(f"Preparing symbol '{self.symbol}': {e}")
|
|
226
|
+
|
|
227
|
+
def summary(self):
|
|
228
|
+
"""Show a brief description about the trading program"""
|
|
229
|
+
print(
|
|
230
|
+
"╔═════════════════ Summary ════════════════════╗\n"
|
|
231
|
+
f"║ Expert Advisor Name @{self.expert_name}\n"
|
|
232
|
+
f"║ Expert Advisor Version @{self.version}\n"
|
|
233
|
+
f"║ Expert | Strategy ID {self.expert_id}\n"
|
|
234
|
+
f"║ Trading Symbol {self.symbol}\n"
|
|
235
|
+
f"║ Trading Time Frame {self.tf}\n"
|
|
236
|
+
f"║ Start Trading Time {self.start_time_hour}:{self.start_time_minutes}\n"
|
|
237
|
+
f"║ Finishing Trading Time {self.finishing_time_hour}:{self.finishing_time_minutes}\n"
|
|
238
|
+
f"║ Closing Position After {self.ending_time_hour}:{self.ending_time_minutes}\n"
|
|
239
|
+
"╚═══════════════════════════════════════════════╝\n"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def risk_managment(self):
|
|
243
|
+
"""Show the risk management parameters"""
|
|
244
|
+
|
|
245
|
+
loss = self.currency_risk()["trade_loss"]
|
|
246
|
+
profit = self.currency_risk()["trade_profit"]
|
|
247
|
+
ok = "OK" if self.is_risk_ok() else "Not OK"
|
|
248
|
+
account_info = self.get_account_info()
|
|
249
|
+
_profit = round(self.get_stats()[1]["total_profit"], 2)
|
|
250
|
+
currency = account_info.currency
|
|
251
|
+
rates = self.get_currency_rates(self.symbol)
|
|
252
|
+
marging_currency = rates['mc']
|
|
253
|
+
print(
|
|
254
|
+
"╔═════════════════ Risk Management ═════════════════════╗\n"
|
|
255
|
+
f"║ Account Name {account_info.name}\n"
|
|
256
|
+
f"║ Account Number {account_info.login}\n"
|
|
257
|
+
f"║ Account Server {account_info.server}\n"
|
|
258
|
+
f"║ Account Balance {account_info.balance} {currency}\n"
|
|
259
|
+
f"║ Account Profit {_profit} {currency}\n"
|
|
260
|
+
f"║ Account Equity {account_info.equity} {currency}\n"
|
|
261
|
+
f"║ Account Leverage {self.get_leverage(True)}\n"
|
|
262
|
+
f"║ Account Margin {round(account_info.margin, 2)} {currency}\n"
|
|
263
|
+
f"║ Account Free Margin {account_info.margin_free} {currency}\n"
|
|
264
|
+
f"║ Maximum Drawdown {self.max_risk}%\n"
|
|
265
|
+
f"║ Risk Allowed {round((self.max_risk - self.risk_level()), 2)}%\n"
|
|
266
|
+
f"║ Volume {self.volume()} {marging_currency}\n"
|
|
267
|
+
f"║ Risk Per trade {-self.get_currency_risk()} {currency}\n"
|
|
268
|
+
f"║ Profit Expected Per trade {self.expected_profit()} {currency}\n"
|
|
269
|
+
f"║ Lot Size {self.lot} Lots\n"
|
|
270
|
+
f"║ Stop Loss {self.stop_loss} Points\n"
|
|
271
|
+
f"║ Loss Value Per Tick {round(loss, 5)} {currency}\n"
|
|
272
|
+
f"║ Take Profit {self.take_profit} Points\n"
|
|
273
|
+
f"║ Profit Value Per Tick {round(profit, 5)} {currency}\n"
|
|
274
|
+
f"║ Break Even {self.break_even_points} Points\n"
|
|
275
|
+
f"║ Deviation {self.deviation} Points\n"
|
|
276
|
+
f"║ Trading Time Interval {self.get_minutes()} Minutes\n"
|
|
277
|
+
f"║ Risk Level {ok}\n"
|
|
278
|
+
f"║ Maximum Trades {self.max_trade()}\n"
|
|
279
|
+
"╚══════════════════════════════════════════════════════╝\n"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def statistics(self, save=True, dir="stats"):
|
|
283
|
+
"""
|
|
284
|
+
Print some statistics for the trading session and save to CSV if specified.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
save (bool, optional): Whether to save the statistics to a CSV file.
|
|
288
|
+
dir (str, optional): The directory to save the CSV file.
|
|
289
|
+
"""
|
|
290
|
+
stats, additional_stats = self.get_stats()
|
|
291
|
+
|
|
292
|
+
deals = stats["deals"]
|
|
293
|
+
wins = stats["win_trades"]
|
|
294
|
+
losses = stats["loss_trades"]
|
|
295
|
+
profit = round(stats["profit"], 2)
|
|
296
|
+
win_rate = stats["win_rate"]
|
|
297
|
+
total_fees = round(stats["total_fees"], 3)
|
|
298
|
+
average_fee = round(stats["average_fee"], 3)
|
|
299
|
+
profitability = additional_stats["profitability"]
|
|
300
|
+
currency = self.get_account_info().currency
|
|
301
|
+
net_profit = round((profit + total_fees), 2)
|
|
302
|
+
trade_risk = round(self.get_currency_risk() * -1, 2)
|
|
303
|
+
expected_profit = round((trade_risk * self.rr * -1), 2)
|
|
304
|
+
|
|
305
|
+
# Formatting the statistics output
|
|
306
|
+
stats_output = (
|
|
307
|
+
f"╔═══════════════ Session Statistics ═════════════╗\n"
|
|
308
|
+
f"║ Total Trades {deals}\n"
|
|
309
|
+
f"║ Winning Trades {wins}\n"
|
|
310
|
+
f"║ Losing Trades {losses}\n"
|
|
311
|
+
f"║ Session Profit {profit} {currency}\n"
|
|
312
|
+
f"║ Total Fees {total_fees} {currency}\n"
|
|
313
|
+
f"║ Average Fees {average_fee} {currency}\n"
|
|
314
|
+
f"║ Net Profit {net_profit} {currency}\n"
|
|
315
|
+
f"║ Risk per Trade {trade_risk} {currency}\n"
|
|
316
|
+
f"║ Expected Profit per Trade {self.expected_profit()} {currency}\n"
|
|
317
|
+
f"║ Risk Reward Ratio {self.rr}\n"
|
|
318
|
+
f"║ Win Rate {win_rate}%\n"
|
|
319
|
+
f"║ Sharpe Ratio {self.sharpe()}\n"
|
|
320
|
+
f"║ Trade Profitability {profitability}\n"
|
|
321
|
+
"╚═════════════════════════════════════════════════╝\n"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Print the formatted statistics
|
|
325
|
+
if self.verbose:
|
|
326
|
+
print(stats_output)
|
|
327
|
+
|
|
328
|
+
# Save to CSV if specified
|
|
329
|
+
if save:
|
|
330
|
+
today_date = datetime.now().strftime("%Y-%m-%d")
|
|
331
|
+
# Create a dictionary with the statistics
|
|
332
|
+
statistics_dict = {
|
|
333
|
+
"Total Trades": deals,
|
|
334
|
+
"Winning Trades": wins,
|
|
335
|
+
"Losing Trades": losses,
|
|
336
|
+
"Session Profit": f"{profit} {currency}",
|
|
337
|
+
"Total Fees": f"{total_fees} {currency}",
|
|
338
|
+
"Average Fees": f"{average_fee} {currency}",
|
|
339
|
+
"Net Profit": f"{net_profit} {currency}",
|
|
340
|
+
"Risk per Trade": f"{trade_risk} {currency}",
|
|
341
|
+
"Expected Profit per Trade": f"{expected_profit} {currency}",
|
|
342
|
+
"Risk Reward Ratio": self.rr,
|
|
343
|
+
"Win Rate": f"{win_rate}%",
|
|
344
|
+
"Sharpe Ratio": self.sharpe(),
|
|
345
|
+
"Trade Profitability": profitability,
|
|
346
|
+
}
|
|
347
|
+
# Create the directory if it doesn't exist
|
|
348
|
+
os.makedirs(dir, exist_ok=True)
|
|
349
|
+
if '.' in self.symbol:
|
|
350
|
+
symbol = self.symbol.split('.')[0]
|
|
351
|
+
else:
|
|
352
|
+
symbol = self.symbol
|
|
353
|
+
|
|
354
|
+
filename = f"{symbol}_{today_date}_session.csv"
|
|
355
|
+
filepath = os.path.join(dir, filename)
|
|
356
|
+
|
|
357
|
+
# Updated code to write to CSV
|
|
358
|
+
with open(filepath, mode="w", newline='', encoding='utf-8') as csv_file:
|
|
359
|
+
writer = csv.writer(
|
|
360
|
+
csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL
|
|
361
|
+
)
|
|
362
|
+
writer.writerow(["Statistic", "Value"])
|
|
363
|
+
for stat, value in statistics_dict.items():
|
|
364
|
+
writer.writerow([stat, value])
|
|
365
|
+
logger.info(f"Session statistics saved to {filepath}")
|
|
366
|
+
|
|
367
|
+
def open_buy_position(
|
|
368
|
+
self,
|
|
369
|
+
action: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT'] = 'BMKT',
|
|
370
|
+
price: Optional[float] = None,
|
|
371
|
+
mm: bool = True,
|
|
372
|
+
id: Optional[int] = None,
|
|
373
|
+
comment: Optional[str] = None
|
|
374
|
+
):
|
|
375
|
+
"""
|
|
376
|
+
Open a Buy positin
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
action (str): `'BMKT'` for Market orders or `'BLMT',
|
|
380
|
+
'BSTP','BSTPLMT'` for pending orders
|
|
381
|
+
price (float): The price at which to open an order
|
|
382
|
+
id (int): The strategy id or expert Id
|
|
383
|
+
mm (bool): Weither to put stop loss and tp or not
|
|
384
|
+
comment (str): The comment for the opening position
|
|
385
|
+
"""
|
|
386
|
+
Id = id if id is not None else self.expert_id
|
|
387
|
+
point = self.get_symbol_info(self.symbol).point
|
|
388
|
+
if action != 'BMKT':
|
|
389
|
+
assert price is not None, \
|
|
390
|
+
"You need to set a price for pending orders"
|
|
391
|
+
_price = price
|
|
392
|
+
else:
|
|
393
|
+
_price = self.get_tick_info(self.symbol).ask
|
|
394
|
+
digits = self.get_symbol_info(self.symbol).digits
|
|
395
|
+
|
|
396
|
+
lot = self.get_lot()
|
|
397
|
+
stop_loss = self.get_stop_loss()
|
|
398
|
+
take_profit = self.get_take_profit()
|
|
399
|
+
deviation = self.get_deviation()
|
|
400
|
+
request = {
|
|
401
|
+
"action": Mt5.TRADE_ACTION_DEAL,
|
|
402
|
+
"symbol": self.symbol,
|
|
403
|
+
"volume": float(lot),
|
|
404
|
+
"type": Mt5.ORDER_TYPE_BUY,
|
|
405
|
+
"price": _price,
|
|
406
|
+
"deviation": deviation,
|
|
407
|
+
"magic": Id,
|
|
408
|
+
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
409
|
+
"type_time": Mt5.ORDER_TIME_GTC,
|
|
410
|
+
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
411
|
+
}
|
|
412
|
+
if mm:
|
|
413
|
+
request['sl'] = (_price - stop_loss * point)
|
|
414
|
+
request['tp'] = (_price + take_profit * point)
|
|
415
|
+
if action != 'BMKT':
|
|
416
|
+
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
417
|
+
request["type"] = self._order_type()[action][0]
|
|
418
|
+
|
|
419
|
+
self.break_even(comment)
|
|
420
|
+
if self.check(comment):
|
|
421
|
+
self.request_result(_price, request, action),
|
|
422
|
+
|
|
423
|
+
def _order_type(self):
|
|
424
|
+
type = {
|
|
425
|
+
'BMKT': (Mt5.ORDER_TYPE_BUY, 'BUY'),
|
|
426
|
+
'SMKT': (Mt5.ORDER_TYPE_BUY, 'SELL'),
|
|
427
|
+
'BLMT': (Mt5.ORDER_TYPE_BUY_LIMIT, 'BUY_LIMIT'),
|
|
428
|
+
'SLMT': (Mt5.ORDER_TYPE_SELL_LIMIT, 'SELL_LIMIT'),
|
|
429
|
+
'BSTP': (Mt5.ORDER_TYPE_BUY_STOP, 'BUY_STOP'),
|
|
430
|
+
'SSTP': (Mt5.ORDER_TYPE_SELL_STOP, 'SELL_STOP'),
|
|
431
|
+
'BSTPLMT': (Mt5.ORDER_TYPE_BUY_STOP_LIMIT, 'BUY_STOP_LIMIT'),
|
|
432
|
+
'SSTPLMT': (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, 'SELL_STOP_LIMIT')
|
|
433
|
+
}
|
|
434
|
+
return type
|
|
435
|
+
|
|
436
|
+
def open_sell_position(
|
|
437
|
+
self,
|
|
438
|
+
action: Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT'] = 'SMKT',
|
|
439
|
+
price: Optional[float] = None,
|
|
440
|
+
mm: bool = True,
|
|
441
|
+
id: Optional[int] = None,
|
|
442
|
+
comment: Optional[str] = None
|
|
443
|
+
):
|
|
444
|
+
"""
|
|
445
|
+
Open a sell positin
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
action (str): `'SMKT'` for Market orders
|
|
449
|
+
or `'SLMT', 'SSTP','SSTPLMT'` for pending orders
|
|
450
|
+
price (float): The price at which to open an order
|
|
451
|
+
id (int): The strategy id or expert Id
|
|
452
|
+
mm (bool): Weither to put stop loss and tp or not
|
|
453
|
+
comment (str): The comment for the closing position
|
|
454
|
+
"""
|
|
455
|
+
Id = id if id is not None else self.expert_id
|
|
456
|
+
point = self.get_symbol_info(self.symbol).point
|
|
457
|
+
if action != 'SMKT':
|
|
458
|
+
assert price is not None, \
|
|
459
|
+
"You need to set a price for pending orders"
|
|
460
|
+
_price = price
|
|
461
|
+
else:
|
|
462
|
+
_price = self.get_tick_info(self.symbol).bid
|
|
463
|
+
digits = self.get_symbol_info(self.symbol).digits
|
|
464
|
+
|
|
465
|
+
lot = self.get_lot()
|
|
466
|
+
stop_loss = self.get_stop_loss()
|
|
467
|
+
take_profit = self.get_take_profit()
|
|
468
|
+
deviation = self.get_deviation()
|
|
469
|
+
request = {
|
|
470
|
+
"action": Mt5.TRADE_ACTION_DEAL,
|
|
471
|
+
"symbol": self.symbol,
|
|
472
|
+
"volume": float(lot),
|
|
473
|
+
"type": Mt5.ORDER_TYPE_SELL,
|
|
474
|
+
"price": _price,
|
|
475
|
+
"deviation": deviation,
|
|
476
|
+
"magic": Id,
|
|
477
|
+
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
478
|
+
"type_time": Mt5.ORDER_TIME_GTC,
|
|
479
|
+
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
480
|
+
}
|
|
481
|
+
if mm:
|
|
482
|
+
request["sl"] = (_price + stop_loss * point)
|
|
483
|
+
request["tp"] = (_price - take_profit * point)
|
|
484
|
+
if action != 'SMKT':
|
|
485
|
+
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
486
|
+
request["type"] = self._order_type()[action][0]
|
|
487
|
+
|
|
488
|
+
self.break_even(comment)
|
|
489
|
+
if self.check(comment):
|
|
490
|
+
self.request_result(_price, request, action)
|
|
491
|
+
|
|
492
|
+
def _risk_free(self):
|
|
493
|
+
max_trade = self.max_trade()
|
|
494
|
+
loss_trades = self.get_stats()[0]['loss_trades']
|
|
495
|
+
if loss_trades >= max_trade:
|
|
496
|
+
return False
|
|
497
|
+
return True
|
|
498
|
+
|
|
499
|
+
def check(self, comment):
|
|
500
|
+
"""
|
|
501
|
+
Verify if all conditions for taking a position are valide,
|
|
502
|
+
These conditions are based on the Maximum risk ,daily risk,
|
|
503
|
+
the starting, the finishing, and ending trading time.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
comment (str): The comment for the closing position
|
|
507
|
+
"""
|
|
508
|
+
if self.days_end():
|
|
509
|
+
return False
|
|
510
|
+
elif not self.trading_time():
|
|
511
|
+
logger.info(f"Not Trading time, SYMBOL={self.symbol}")
|
|
512
|
+
return False
|
|
513
|
+
elif not self.is_risk_ok():
|
|
514
|
+
logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
|
|
515
|
+
self._check(comment)
|
|
516
|
+
return False
|
|
517
|
+
elif not self._risk_free():
|
|
518
|
+
logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
|
|
519
|
+
self._check(comment)
|
|
520
|
+
return False
|
|
521
|
+
elif self.profit_target():
|
|
522
|
+
self._check(f'Profit target Reached !!! SYMBOL={self.symbol}')
|
|
523
|
+
return True
|
|
524
|
+
|
|
525
|
+
def _check(self, txt: str = ""):
|
|
526
|
+
if self.positive_profit() or self.get_current_open_positions() is None:
|
|
527
|
+
self.close_positions(position_type='all')
|
|
528
|
+
logger.info(txt)
|
|
529
|
+
time.sleep(5)
|
|
530
|
+
self.statistics(save=True)
|
|
531
|
+
|
|
532
|
+
def request_result(
|
|
533
|
+
self,
|
|
534
|
+
price: float,
|
|
535
|
+
request: Dict[str, Any],
|
|
536
|
+
type: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
|
|
537
|
+
'SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
538
|
+
):
|
|
539
|
+
"""
|
|
540
|
+
Check if a trading order has been sent correctly
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
price (float): Price for opening the position
|
|
544
|
+
request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
|
|
545
|
+
all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
|
|
546
|
+
|
|
547
|
+
type (str): The type of the order
|
|
548
|
+
`(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
|
|
549
|
+
"""
|
|
550
|
+
# Send a trading request
|
|
551
|
+
# Check the execution result
|
|
552
|
+
pos = self._order_type()[type][1]
|
|
553
|
+
addtionnal = f", SYMBOL={self.symbol}"
|
|
554
|
+
try:
|
|
555
|
+
check_result = self.check_order(request)
|
|
556
|
+
result = self.send_order(request)
|
|
557
|
+
except Exception as e:
|
|
558
|
+
print(f"{self.get_current_time()} -", end=' ')
|
|
559
|
+
trade_retcode_message(
|
|
560
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
561
|
+
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
562
|
+
msg = trade_retcode_message(result.retcode)
|
|
563
|
+
logger.error(
|
|
564
|
+
f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
565
|
+
if result.retcode in [
|
|
566
|
+
Mt5.TRADE_RETCODE_CONNECTION, Mt5.TRADE_RETCODE_TIMEOUT]:
|
|
567
|
+
tries = 0
|
|
568
|
+
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
569
|
+
time.sleep(1)
|
|
570
|
+
try:
|
|
571
|
+
check_result = self.check_order(request)
|
|
572
|
+
result = self.send_order(request)
|
|
573
|
+
except Exception as e:
|
|
574
|
+
print(f"{self.get_current_time()} -", end=' ')
|
|
575
|
+
trade_retcode_message(
|
|
576
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
577
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
578
|
+
break
|
|
579
|
+
tries += 1
|
|
580
|
+
# Print the result
|
|
581
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
582
|
+
msg = trade_retcode_message(result.retcode)
|
|
583
|
+
logger.info(f"Trade Order {msg}{addtionnal}")
|
|
584
|
+
if type != "BMKT" or type != "SMKT":
|
|
585
|
+
self.opened_orders.append(result.order)
|
|
586
|
+
long_msg = (
|
|
587
|
+
f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{price}, "
|
|
588
|
+
f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
|
|
589
|
+
f"Tp: {self.get_take_profit()}"
|
|
590
|
+
)
|
|
591
|
+
logger.info(long_msg)
|
|
592
|
+
time.sleep(0.1)
|
|
593
|
+
if type == "BMKT" or type == "SMKT":
|
|
594
|
+
self.opened_positions.append(result.order)
|
|
595
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
596
|
+
for position in positions:
|
|
597
|
+
if position.ticket == result.order:
|
|
598
|
+
if position.type == 0:
|
|
599
|
+
order_type = "BUY"
|
|
600
|
+
self.buy_positions.append(position.ticket)
|
|
601
|
+
else:
|
|
602
|
+
order_type = "SELL"
|
|
603
|
+
self.sell_positions.append(position.ticket)
|
|
604
|
+
profit = round(self.get_account_info().profit, 5)
|
|
605
|
+
order_info = (
|
|
606
|
+
f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open,5)}, "
|
|
607
|
+
f"Sl: @{position.sl} Tp: @{position.tp}"
|
|
608
|
+
)
|
|
609
|
+
logger.info(order_info)
|
|
610
|
+
pos_info = (
|
|
611
|
+
f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
|
|
612
|
+
f"{self.get_account_info().currency}]\n"
|
|
613
|
+
)
|
|
614
|
+
logger.info(pos_info)
|
|
615
|
+
|
|
616
|
+
def open_position(
|
|
617
|
+
self,
|
|
618
|
+
action: Literal[
|
|
619
|
+
'BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
|
|
620
|
+
'SMKT', 'SLMT', 'SSTP', 'SSTPLMT'],
|
|
621
|
+
buy: bool = False,
|
|
622
|
+
sell: bool = False,
|
|
623
|
+
price: Optional[float] = None,
|
|
624
|
+
id: Optional[int] = None,
|
|
625
|
+
mm: bool = True,
|
|
626
|
+
comment: Optional[str] = None
|
|
627
|
+
):
|
|
628
|
+
"""
|
|
629
|
+
Open a buy or sell position.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
action (str): (`'BMKT'`, `'SMKT'`) for Market orders
|
|
633
|
+
or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
|
|
634
|
+
buy (bool): A boolean True or False
|
|
635
|
+
sell (bool): A boolean True or False
|
|
636
|
+
id (int): The strategy id or expert Id
|
|
637
|
+
mm (bool): Weither to put stop loss and tp or not
|
|
638
|
+
comment (str): The comment for the closing position
|
|
639
|
+
"""
|
|
640
|
+
if buy:
|
|
641
|
+
self.open_buy_position(
|
|
642
|
+
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
643
|
+
if sell:
|
|
644
|
+
self.open_sell_position(
|
|
645
|
+
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
646
|
+
|
|
647
|
+
@property
|
|
648
|
+
def get_opened_orders(self):
|
|
649
|
+
""" Return all opened order's tickets"""
|
|
650
|
+
if len(self.opened_orders) != 0:
|
|
651
|
+
return self.opened_orders
|
|
652
|
+
return None
|
|
653
|
+
|
|
654
|
+
@property
|
|
655
|
+
def get_opened_positions(self):
|
|
656
|
+
"""Return all opened position's tickets"""
|
|
657
|
+
if len(self.opened_positions) != 0:
|
|
658
|
+
return self.opened_positions
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
@property
|
|
662
|
+
def get_buy_positions(self):
|
|
663
|
+
"""Return all buy opened position's tickets"""
|
|
664
|
+
if len(self.buy_positions) != 0:
|
|
665
|
+
return self.buy_positions
|
|
666
|
+
return None
|
|
667
|
+
|
|
668
|
+
@property
|
|
669
|
+
def get_sell_positions(self):
|
|
670
|
+
"""Return all sell opened position's tickets"""
|
|
671
|
+
if len(self.sell_positions) != 0:
|
|
672
|
+
return self.sell_positions
|
|
673
|
+
return None
|
|
674
|
+
|
|
675
|
+
@property
|
|
676
|
+
def get_be_positions(self):
|
|
677
|
+
"""Return All positon's tickets
|
|
678
|
+
for which a break even has been set"""
|
|
679
|
+
if len(self.break_even_status) != 0:
|
|
680
|
+
return self.break_even_status
|
|
681
|
+
return None
|
|
682
|
+
|
|
683
|
+
def get_filtered_tickets(self,
|
|
684
|
+
id: Optional[int] = None,
|
|
685
|
+
filter_type: Optional[str] = None,
|
|
686
|
+
th=None
|
|
687
|
+
) -> List[int] | None:
|
|
688
|
+
"""
|
|
689
|
+
Get tickets for positions or orders based on filters.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
id (int): The strategy id or expert Id
|
|
693
|
+
filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'win_trades')
|
|
694
|
+
- `orders` are current open orders
|
|
695
|
+
- `positions` are all current open positions
|
|
696
|
+
- `buys` and `sells` are current buy or sell open positions
|
|
697
|
+
- `win_trades` are current open position that have a profit greater than a threshold
|
|
698
|
+
th (bool): the minimum treshold for winning position
|
|
699
|
+
(only relevant when filter_type is 'win_trades')
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
List[int] | None: A list of filtered tickets
|
|
703
|
+
or None if no tickets match the criteria.
|
|
704
|
+
"""
|
|
705
|
+
Id = id if id is not None else self.expert_id
|
|
706
|
+
|
|
707
|
+
if filter_type == 'orders':
|
|
708
|
+
items = self.get_orders(symbol=self.symbol)
|
|
709
|
+
else:
|
|
710
|
+
items = self.get_positions(symbol=self.symbol)
|
|
711
|
+
|
|
712
|
+
filtered_tickets = []
|
|
713
|
+
|
|
714
|
+
if items is not None:
|
|
715
|
+
for item in items:
|
|
716
|
+
if item.magic == Id:
|
|
717
|
+
if filter_type == 'buys' and item.type != 0:
|
|
718
|
+
continue
|
|
719
|
+
if filter_type == 'sells' and item.type != 1:
|
|
720
|
+
continue
|
|
721
|
+
if filter_type == 'win_trades' and not self.win_trade(item, th=th):
|
|
722
|
+
continue
|
|
723
|
+
filtered_tickets.append(item.ticket)
|
|
724
|
+
return filtered_tickets if filtered_tickets else None
|
|
725
|
+
return None
|
|
726
|
+
|
|
727
|
+
def get_current_open_orders(self, id: Optional[int] = None) -> List[int] | None:
|
|
728
|
+
return self.get_filtered_tickets(id=id, filter_type='orders')
|
|
729
|
+
|
|
730
|
+
def get_current_open_positions(self, id: Optional[int] = None) -> List[int] | None:
|
|
731
|
+
return self.get_filtered_tickets(id=id, filter_type='positions')
|
|
732
|
+
|
|
733
|
+
def get_current_win_trades(self, id: Optional[int] = None, th=None) -> List[int] | None:
|
|
734
|
+
return self.get_filtered_tickets(id=id, filter_type='win_trades', th=th)
|
|
735
|
+
|
|
736
|
+
def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
|
|
737
|
+
return self.get_filtered_tickets(id=id, filter_type='buys')
|
|
738
|
+
|
|
739
|
+
def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
|
|
740
|
+
return self.get_filtered_tickets(id=id, filter_type='sells')
|
|
741
|
+
|
|
742
|
+
def positive_profit(self, th: Optional[float] = None
|
|
743
|
+
) -> bool:
|
|
744
|
+
"""
|
|
745
|
+
Check is the total profit on current open positions
|
|
746
|
+
Is greater than a minimum profit express as percentage
|
|
747
|
+
of the profit target.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
th (float): The minimum profit target on current positions
|
|
751
|
+
"""
|
|
752
|
+
positions = self.get_current_open_positions()
|
|
753
|
+
profit = 0.0
|
|
754
|
+
balance = self.get_account_info().balance
|
|
755
|
+
target = round((balance * self.target)/100, 2)
|
|
756
|
+
if positions is not None:
|
|
757
|
+
for position in positions:
|
|
758
|
+
time.sleep(0.1)
|
|
759
|
+
history = self.get_positions(
|
|
760
|
+
ticket=position
|
|
761
|
+
)
|
|
762
|
+
profit += history[0].profit
|
|
763
|
+
fees = self.get_stats()[0]["average_fee"] * len(positions)
|
|
764
|
+
current_profit = profit + fees
|
|
765
|
+
th_profit = (target*th)/100 if th is not None else (target*0.01)
|
|
766
|
+
if current_profit > th_profit:
|
|
767
|
+
return True
|
|
768
|
+
return False
|
|
769
|
+
|
|
770
|
+
def break_even(self, id: Optional[int] = None):
|
|
771
|
+
"""
|
|
772
|
+
Checks if it's time to put the break even,
|
|
773
|
+
if so , it will sets the break even ,and if the break even was already set,
|
|
774
|
+
it checks if the price has moved in favorable direction,
|
|
775
|
+
if so , it set the new break even.
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
id (int): The strategy Id or Expert Id
|
|
779
|
+
"""
|
|
780
|
+
time.sleep(0.1)
|
|
781
|
+
Id = id if id is not None else self.expert_id
|
|
782
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
783
|
+
be = self.get_break_even()
|
|
784
|
+
if positions is not None:
|
|
785
|
+
for position in positions:
|
|
786
|
+
if position.magic == Id:
|
|
787
|
+
size = self.get_symbol_info(self.symbol).trade_tick_size
|
|
788
|
+
value = self.get_symbol_info(self.symbol).trade_tick_value
|
|
789
|
+
point = self.get_symbol_info(self.symbol).point
|
|
790
|
+
digits = self.get_symbol_info(self.symbol).digits
|
|
791
|
+
points = position.profit * (size / value / position.volume)
|
|
792
|
+
break_even = float(points/point) >= be
|
|
793
|
+
if break_even:
|
|
794
|
+
# Check if break-even has already been set for this position
|
|
795
|
+
if position.ticket not in self.break_even_status:
|
|
796
|
+
self.set_break_even(position, be)
|
|
797
|
+
self.break_even_status.append(position.ticket)
|
|
798
|
+
else:
|
|
799
|
+
# Check if the price has moved favorably
|
|
800
|
+
new_be = be * 0.50
|
|
801
|
+
favorable_move = (
|
|
802
|
+
(position.type == 0 and (
|
|
803
|
+
(position.price_current - position.sl) / point) > new_be)
|
|
804
|
+
or
|
|
805
|
+
(position.type == 1 and (
|
|
806
|
+
(position.sl - position.price_current) / point) > new_be)
|
|
807
|
+
)
|
|
808
|
+
if favorable_move:
|
|
809
|
+
# Calculate the new break-even level and price
|
|
810
|
+
if position.type == 0:
|
|
811
|
+
new_level = round(
|
|
812
|
+
position.sl + (new_be * point), digits)
|
|
813
|
+
new_price = round(
|
|
814
|
+
position.sl + ((0.25 * be) * point), digits)
|
|
815
|
+
else:
|
|
816
|
+
new_level = round(
|
|
817
|
+
position.sl - (new_be * point), digits)
|
|
818
|
+
new_price = round(
|
|
819
|
+
position.sl - ((0.25 * be) * point), digits)
|
|
820
|
+
self.set_break_even(
|
|
821
|
+
position, be, price=new_price, level=new_level
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
def set_break_even(self,
|
|
825
|
+
position: TradePosition,
|
|
826
|
+
be: int,
|
|
827
|
+
price: Optional[float] = None,
|
|
828
|
+
level: Optional[float] = None):
|
|
829
|
+
"""
|
|
830
|
+
Sets the break-even level for a given trading position.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
position (TradePosition):
|
|
834
|
+
The trading position for which the break-even is to be set
|
|
835
|
+
This is the value return by `mt5.positions_get()`
|
|
836
|
+
be (int): The break-even level in points.
|
|
837
|
+
level (float): The break-even level in price
|
|
838
|
+
if set to None , it will be calated automaticaly.
|
|
839
|
+
price (float): The break-even price
|
|
840
|
+
if set to None , it will be calated automaticaly.
|
|
841
|
+
"""
|
|
842
|
+
point = self.get_symbol_info(self.symbol).point
|
|
843
|
+
digits = self.get_symbol_info(self.symbol).digits
|
|
844
|
+
spread = self.get_symbol_info(self.symbol).spread
|
|
845
|
+
fees = self.get_stats()[0]["average_fee"] * -1
|
|
846
|
+
risk = self.currency_risk()["trade_profit"]
|
|
847
|
+
fees_points = round((fees / risk), 3)
|
|
848
|
+
# If Buy
|
|
849
|
+
if position.type == 0 and position.price_current > position.price_open:
|
|
850
|
+
# Calculate the break-even level and price
|
|
851
|
+
break_even_level = position.price_open + (be * point)
|
|
852
|
+
break_even_price = position.price_open + \
|
|
853
|
+
((fees_points + spread) * point)
|
|
854
|
+
_price = break_even_price if price is None else price
|
|
855
|
+
_level = break_even_level if level is None else level
|
|
856
|
+
|
|
857
|
+
if self.get_tick_info(self.symbol).ask > _level:
|
|
858
|
+
# Set the stop loss to break even
|
|
859
|
+
request = {
|
|
860
|
+
"action": Mt5.TRADE_ACTION_SLTP,
|
|
861
|
+
"type": Mt5.ORDER_TYPE_SELL_STOP,
|
|
862
|
+
"position": position.ticket,
|
|
863
|
+
"sl": round(_price, digits),
|
|
864
|
+
"tp": position.tp
|
|
865
|
+
}
|
|
866
|
+
self._break_even_request(
|
|
867
|
+
position.ticket, round(_price, digits), request)
|
|
868
|
+
# If Sell
|
|
869
|
+
elif position.type == 1 and position.price_current < position.price_open:
|
|
870
|
+
break_even_level = position.price_open - (be * point)
|
|
871
|
+
break_even_price = position.price_open - \
|
|
872
|
+
((fees_points + spread) * point)
|
|
873
|
+
_price = break_even_price if price is None else price
|
|
874
|
+
_level = break_even_level if level is None else level
|
|
875
|
+
|
|
876
|
+
if self.get_tick_info(self.symbol).bid < _level:
|
|
877
|
+
# Set the stop loss to break even
|
|
878
|
+
request = {
|
|
879
|
+
"action": Mt5.TRADE_ACTION_SLTP,
|
|
880
|
+
"type": Mt5.ORDER_TYPE_BUY_STOP,
|
|
881
|
+
"position": position.ticket,
|
|
882
|
+
"sl": round(_price, digits),
|
|
883
|
+
"tp": position.tp
|
|
884
|
+
}
|
|
885
|
+
self._break_even_request(
|
|
886
|
+
position.ticket, round(_price, digits), request)
|
|
887
|
+
|
|
888
|
+
def _break_even_request(self, tiket, price, request):
|
|
889
|
+
"""
|
|
890
|
+
Send a request to set the stop loss to break even for a given trading position.
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
tiket (int): The ticket number of the trading position.
|
|
894
|
+
price (float): The price at which the stop loss is to be set.
|
|
895
|
+
request (dict): The request to set the stop loss to break even.
|
|
896
|
+
"""
|
|
897
|
+
addtionnal = f", SYMBOL={self.symbol}"
|
|
898
|
+
time.sleep(0.1)
|
|
899
|
+
try:
|
|
900
|
+
check_result = self.check_order(request)
|
|
901
|
+
result = self.send_order(request)
|
|
902
|
+
except Exception as e:
|
|
903
|
+
print(f"{self.get_current_time()} -", end=' ')
|
|
904
|
+
trade_retcode_message(
|
|
905
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
906
|
+
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
907
|
+
msg = trade_retcode_message(result.retcode)
|
|
908
|
+
logger.error(
|
|
909
|
+
f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
910
|
+
tries = 0
|
|
911
|
+
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
|
|
912
|
+
if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
913
|
+
break
|
|
914
|
+
else:
|
|
915
|
+
time.sleep(1)
|
|
916
|
+
try:
|
|
917
|
+
check_result = self.check_order(request)
|
|
918
|
+
result = self.send_order(request)
|
|
919
|
+
except Exception as e:
|
|
920
|
+
print(f"{self.get_current_time()} -", end=' ')
|
|
921
|
+
trade_retcode_message(
|
|
922
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
923
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
924
|
+
break
|
|
925
|
+
tries += 1
|
|
926
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
927
|
+
msg = trade_retcode_message(result.retcode)
|
|
928
|
+
logger.info(f"Break-Even Order {msg}{addtionnal}")
|
|
929
|
+
info = (
|
|
930
|
+
f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
|
|
931
|
+
)
|
|
932
|
+
logger.info(info)
|
|
933
|
+
self.break_even_status.append(tiket)
|
|
934
|
+
|
|
935
|
+
def win_trade(self,
|
|
936
|
+
position: TradePosition,
|
|
937
|
+
th: Optional[int] = None) -> bool:
|
|
938
|
+
"""
|
|
939
|
+
Check if a positon is wining or looing
|
|
940
|
+
wen it is closed before be level , tp or sl.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
th (int): The minimum profit for a position in point
|
|
944
|
+
"""
|
|
945
|
+
size = self.get_symbol_info(self.symbol).trade_tick_size
|
|
946
|
+
value = self.get_symbol_info(self.symbol).trade_tick_value
|
|
947
|
+
points = position.profit * (size / value / position.volume)
|
|
948
|
+
|
|
949
|
+
spread = self.get_symbol_info(self.symbol).spread
|
|
950
|
+
point = self.get_symbol_info(self.symbol).point
|
|
951
|
+
fees = self.get_stats()[0]["average_fee"] * -1
|
|
952
|
+
risk = self.currency_risk()["trade_profit"]
|
|
953
|
+
min_be = round((fees / risk)) + 2
|
|
954
|
+
be = self.get_break_even()
|
|
955
|
+
if th is not None:
|
|
956
|
+
win_be = th
|
|
957
|
+
else:
|
|
958
|
+
win_be = max(min_be, round((0.1 * be)))
|
|
959
|
+
win_trade = float(points/point) >= be
|
|
960
|
+
# Check if the positon is in profit
|
|
961
|
+
if win_trade:
|
|
962
|
+
# Check if break-even has already been set for this position
|
|
963
|
+
if position.ticket not in self.break_even_status:
|
|
964
|
+
return True
|
|
965
|
+
return False
|
|
966
|
+
|
|
967
|
+
def profit_target(self):
|
|
968
|
+
fee = 0.0
|
|
969
|
+
swap = 0.0
|
|
970
|
+
commission = 0.0
|
|
971
|
+
profit = 0.0
|
|
972
|
+
balance = self.get_account_info().balance
|
|
973
|
+
target = round((balance * self.target)/100, 2)
|
|
974
|
+
if len(self.opened_positions) != 0:
|
|
975
|
+
for position in self.opened_positions:
|
|
976
|
+
time.sleep(0.1)
|
|
977
|
+
# This return two TradeDeal Object,
|
|
978
|
+
# The first one is the one the opening order
|
|
979
|
+
# The second is the closing order
|
|
980
|
+
history = self.get_trades_history(
|
|
981
|
+
position=position, to_df=False
|
|
982
|
+
)
|
|
983
|
+
if len(history) == 2:
|
|
984
|
+
profit += history[1].profit
|
|
985
|
+
commission += history[0].commission
|
|
986
|
+
swap += history[0].swap
|
|
987
|
+
fee += history[0].fee
|
|
988
|
+
current_profit = profit + commission + fee + swap
|
|
989
|
+
if current_profit >= target:
|
|
990
|
+
return True
|
|
991
|
+
return False
|
|
992
|
+
|
|
993
|
+
def close_position(self,
|
|
994
|
+
ticket: int,
|
|
995
|
+
id: Optional[int] = None,
|
|
996
|
+
pct: Optional[float] = 1.0,
|
|
997
|
+
comment: Optional[str] = None
|
|
998
|
+
) -> bool:
|
|
999
|
+
"""
|
|
1000
|
+
Close an open position by it ticket
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
ticket (int): Positon ticket to close (e.g TradePosition.ticket)
|
|
1004
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1005
|
+
pct (float): Percentage of the position to close
|
|
1006
|
+
comment (str): Comment for the closing position
|
|
1007
|
+
|
|
1008
|
+
Returns:
|
|
1009
|
+
- True if position closed, False otherwise
|
|
1010
|
+
"""
|
|
1011
|
+
# get all Actives positions
|
|
1012
|
+
time.sleep(0.1)
|
|
1013
|
+
Id = id if id is not None else self.expert_id
|
|
1014
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
1015
|
+
buy_price = self.get_tick_info(self.symbol).ask
|
|
1016
|
+
sell_price = self.get_tick_info(self.symbol).bid
|
|
1017
|
+
digits = self.get_symbol_info(self.symbol).digits
|
|
1018
|
+
deviation = self.get_deviation()
|
|
1019
|
+
if positions is not None:
|
|
1020
|
+
for position in positions:
|
|
1021
|
+
if (position.ticket == ticket
|
|
1022
|
+
and position.magic == Id
|
|
1023
|
+
):
|
|
1024
|
+
buy = position.type == 0
|
|
1025
|
+
sell = position.type == 1
|
|
1026
|
+
request = {
|
|
1027
|
+
"action": Mt5.TRADE_ACTION_DEAL,
|
|
1028
|
+
"symbol": self.symbol,
|
|
1029
|
+
"volume": (position.volume*pct),
|
|
1030
|
+
"type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
|
|
1031
|
+
"position": ticket,
|
|
1032
|
+
"price": sell_price if buy else buy_price,
|
|
1033
|
+
"deviation": deviation,
|
|
1034
|
+
"magic": Id,
|
|
1035
|
+
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
1036
|
+
"type_time": Mt5.ORDER_TIME_GTC,
|
|
1037
|
+
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
1038
|
+
}
|
|
1039
|
+
addtionnal = f", SYMBOL={self.symbol}"
|
|
1040
|
+
try:
|
|
1041
|
+
check_result = self.check_order(request)
|
|
1042
|
+
result = self.send_order(request)
|
|
1043
|
+
except Exception as e:
|
|
1044
|
+
print(f"{self.get_current_time()} -", end=' ')
|
|
1045
|
+
trade_retcode_message(
|
|
1046
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1047
|
+
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1048
|
+
msg = trade_retcode_message(result.retcode)
|
|
1049
|
+
logger.error(
|
|
1050
|
+
f"Closing Order Request, Position: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
1051
|
+
tries = 0
|
|
1052
|
+
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
1053
|
+
time.sleep(1)
|
|
1054
|
+
try:
|
|
1055
|
+
check_result = self.check_order(request)
|
|
1056
|
+
result = self.send_order(request)
|
|
1057
|
+
except Exception as e:
|
|
1058
|
+
print(f"{self.get_current_time()} -", end=' ')
|
|
1059
|
+
trade_retcode_message(
|
|
1060
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1061
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1062
|
+
break
|
|
1063
|
+
tries += 1
|
|
1064
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1065
|
+
msg = trade_retcode_message(result.retcode)
|
|
1066
|
+
logger.info(
|
|
1067
|
+
f"Closing Order {msg}{addtionnal}")
|
|
1068
|
+
info = (
|
|
1069
|
+
f"Position #{ticket} closed, Symbol: {self.symbol}, Price: @{request['price']}")
|
|
1070
|
+
logger.info(info)
|
|
1071
|
+
return True
|
|
1072
|
+
else:
|
|
1073
|
+
return False
|
|
1074
|
+
|
|
1075
|
+
def close_positions(
|
|
1076
|
+
self,
|
|
1077
|
+
position_type: Literal["all", "buy", "sell"] = "all",
|
|
1078
|
+
id: Optional[int] = None,
|
|
1079
|
+
comment: Optional[str] = None):
|
|
1080
|
+
"""
|
|
1081
|
+
Args:
|
|
1082
|
+
position_type (str): Type of positions to close ("all", "buy", "sell")
|
|
1083
|
+
id (int): The unique ID of the Expert or Strategy
|
|
1084
|
+
comment (str): Comment for the closing position
|
|
1085
|
+
"""
|
|
1086
|
+
if position_type == "all":
|
|
1087
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
1088
|
+
elif position_type == "buy":
|
|
1089
|
+
positions = self.get_current_buys()
|
|
1090
|
+
elif position_type == "sell":
|
|
1091
|
+
positions = self.get_current_sells()
|
|
1092
|
+
else:
|
|
1093
|
+
logger.error(f"Invalid position type: {position_type}")
|
|
1094
|
+
return
|
|
1095
|
+
|
|
1096
|
+
if positions is not None:
|
|
1097
|
+
if position_type == 'all':
|
|
1098
|
+
pos_type = ""
|
|
1099
|
+
tickets = [position.ticket for position in positions]
|
|
1100
|
+
else:
|
|
1101
|
+
tickets = positions
|
|
1102
|
+
pos_type = position_type
|
|
1103
|
+
else:
|
|
1104
|
+
tickets = []
|
|
1105
|
+
|
|
1106
|
+
if len(tickets) != 0:
|
|
1107
|
+
for ticket in tickets.copy():
|
|
1108
|
+
if self.close_position(ticket, id=id, comment=comment):
|
|
1109
|
+
tickets.remove(ticket)
|
|
1110
|
+
time.sleep(1)
|
|
1111
|
+
|
|
1112
|
+
if len(tickets) == 0:
|
|
1113
|
+
logger.info(
|
|
1114
|
+
f"ALL {position_type.upper()} Positions closed, SYMBOL={self.symbol}.")
|
|
1115
|
+
else:
|
|
1116
|
+
logger.info(
|
|
1117
|
+
f"{len(tickets)} {position_type.upper()} Positions not closed, SYMBOL={self.symbol}")
|
|
1118
|
+
else:
|
|
1119
|
+
logger.info(
|
|
1120
|
+
f"No {position_type.upper()} Positions to close, SYMBOL={self.symbol}.")
|
|
1121
|
+
|
|
1122
|
+
def get_stats(self) -> Tuple[Dict[str, Any]]:
|
|
1123
|
+
"""
|
|
1124
|
+
get some stats about the trading day and trading history
|
|
1125
|
+
|
|
1126
|
+
:return: tuple[Dict[str, Any]]
|
|
1127
|
+
"""
|
|
1128
|
+
# get history of deals for one trading session
|
|
1129
|
+
profit = 0.0
|
|
1130
|
+
total_fees = 0.0
|
|
1131
|
+
loss_trades = 0
|
|
1132
|
+
win_trades = 0
|
|
1133
|
+
balance = self.get_account_info().balance
|
|
1134
|
+
target = round((balance * self.target)/100)
|
|
1135
|
+
deals = len(self.opened_positions)
|
|
1136
|
+
if deals != 0:
|
|
1137
|
+
for position in self.opened_positions:
|
|
1138
|
+
time.sleep(0.1)
|
|
1139
|
+
history = self.get_trades_history(
|
|
1140
|
+
position=position, to_df=False
|
|
1141
|
+
)
|
|
1142
|
+
if len(history) == 2:
|
|
1143
|
+
result = history[1].profit
|
|
1144
|
+
comm = history[0].commission
|
|
1145
|
+
swap = history[0].swap
|
|
1146
|
+
fee = history[0].fee
|
|
1147
|
+
if (result + comm + swap + fee) <= 0:
|
|
1148
|
+
loss_trades += 1
|
|
1149
|
+
else:
|
|
1150
|
+
win_trades += 1
|
|
1151
|
+
profit += result
|
|
1152
|
+
total_fees += (comm + swap + fee)
|
|
1153
|
+
average_fee = total_fees / deals
|
|
1154
|
+
win_rate = round((win_trades / deals) * 100, 2)
|
|
1155
|
+
stats1 = {
|
|
1156
|
+
"deals": deals,
|
|
1157
|
+
"profit": profit,
|
|
1158
|
+
"win_trades": win_trades,
|
|
1159
|
+
"loss_trades": loss_trades,
|
|
1160
|
+
"total_fees": total_fees,
|
|
1161
|
+
"average_fee": average_fee,
|
|
1162
|
+
"win_rate": win_rate
|
|
1163
|
+
}
|
|
1164
|
+
else:
|
|
1165
|
+
stats1 = {
|
|
1166
|
+
"deals": 0,
|
|
1167
|
+
"profit": 0,
|
|
1168
|
+
"win_trades": 0,
|
|
1169
|
+
"loss_trades": 0,
|
|
1170
|
+
"total_fees": 0,
|
|
1171
|
+
"average_fee": 0,
|
|
1172
|
+
"win_rate": 0,
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
# Get total stats
|
|
1176
|
+
df = self.get_trades_history()
|
|
1177
|
+
if df is not None:
|
|
1178
|
+
df2 = df.iloc[1:]
|
|
1179
|
+
profit = df2["profit"].sum()
|
|
1180
|
+
commisions = df2["commission"].sum()
|
|
1181
|
+
_fees = df2["fee"].sum()
|
|
1182
|
+
_swap = df2["swap"].sum()
|
|
1183
|
+
total_profit = commisions + _fees + _swap + profit
|
|
1184
|
+
account_info = self.get_account_info()
|
|
1185
|
+
balance = account_info.balance
|
|
1186
|
+
initial_balance = balance - total_profit
|
|
1187
|
+
profittable = "Yes" if balance > initial_balance else "No"
|
|
1188
|
+
stats2 = {
|
|
1189
|
+
"total_profit": total_profit,
|
|
1190
|
+
"profitability": profittable
|
|
1191
|
+
}
|
|
1192
|
+
else:
|
|
1193
|
+
stats2 = {
|
|
1194
|
+
"total_profit": 0,
|
|
1195
|
+
"profitability": 0
|
|
1196
|
+
}
|
|
1197
|
+
return (stats1, stats2)
|
|
1198
|
+
|
|
1199
|
+
def sharpe(self):
|
|
1200
|
+
"""
|
|
1201
|
+
Calculate the Sharpe ratio of a returns stream
|
|
1202
|
+
based on a number of trading periods.
|
|
1203
|
+
The function assumes that the returns are the excess of
|
|
1204
|
+
those compared to a benchmark.
|
|
1205
|
+
"""
|
|
1206
|
+
# Get total history
|
|
1207
|
+
df2 = self.get_trades_history()
|
|
1208
|
+
if df2 is None:
|
|
1209
|
+
return 0.0
|
|
1210
|
+
df = df2.iloc[1:]
|
|
1211
|
+
profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
|
|
1212
|
+
returns = profit.values
|
|
1213
|
+
returns = np.diff(returns, prepend=0.0)
|
|
1214
|
+
N = self.max_trade() * 252
|
|
1215
|
+
sharp = np.sqrt(N) * np.mean(returns) / (np.std(returns) + 1e-10)
|
|
1216
|
+
|
|
1217
|
+
return round(sharp, 3)
|
|
1218
|
+
|
|
1219
|
+
def days_end(self) -> bool:
|
|
1220
|
+
"""Check if it is the end of the trading day."""
|
|
1221
|
+
current_hour = datetime.now().hour
|
|
1222
|
+
current_minute = datetime.now().minute
|
|
1223
|
+
|
|
1224
|
+
ending_hour = int(self.ending_time_hour)
|
|
1225
|
+
ending_minute = int(self.ending_time_minutes)
|
|
1226
|
+
|
|
1227
|
+
if current_hour > ending_hour or (
|
|
1228
|
+
current_hour == ending_hour and current_minute >= ending_minute
|
|
1229
|
+
):
|
|
1230
|
+
return True
|
|
1231
|
+
else:
|
|
1232
|
+
return False
|
|
1233
|
+
|
|
1234
|
+
def trading_time(self):
|
|
1235
|
+
"""Check if it is time to trade."""
|
|
1236
|
+
if (
|
|
1237
|
+
int(self.start_time_hour)
|
|
1238
|
+
< datetime.now().hour
|
|
1239
|
+
< int(self.finishing_time_hour)
|
|
1240
|
+
):
|
|
1241
|
+
return True
|
|
1242
|
+
elif datetime.now().hour == int(self.start_time_hour):
|
|
1243
|
+
if datetime.now().minute >= int(self.start_time_minutes):
|
|
1244
|
+
return True
|
|
1245
|
+
elif datetime.now().hour == int(self.finishing_time_hour):
|
|
1246
|
+
if datetime.now().minute < int(self.finishing_time_minutes):
|
|
1247
|
+
return True
|
|
1248
|
+
return False
|
|
1249
|
+
|
|
1250
|
+
def sleep_time(self, weekend=False):
|
|
1251
|
+
if weekend:
|
|
1252
|
+
# claculate number of minute from the friday and to monday start
|
|
1253
|
+
friday_time = datetime.strptime(self.end, '%H:%M')
|
|
1254
|
+
monday_time = datetime.strptime(self.start, '%H:%M')
|
|
1255
|
+
intra_day_diff = (monday_time - friday_time).total_seconds() // 60
|
|
1256
|
+
inter_day_diff = 3 * 24 * 60
|
|
1257
|
+
total_minutes = inter_day_diff + intra_day_diff
|
|
1258
|
+
return total_minutes
|
|
1259
|
+
else:
|
|
1260
|
+
# claculate number of minute from the end to the start
|
|
1261
|
+
start = datetime.strptime(self.start, '%H:%M')
|
|
1262
|
+
end = datetime.strptime(self.end, '%H:%M')
|
|
1263
|
+
minutes = (end - start).total_seconds() // 60
|
|
1264
|
+
sleep_time = (24*60) - minutes
|
|
1265
|
+
return sleep_time
|
|
1266
|
+
|
|
1267
|
+
def get_current_time(self):
|
|
1268
|
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def create_trade_instance(
|
|
1272
|
+
symbols: List[str],
|
|
1273
|
+
params: Dict[str, Any]) -> Dict[str, Trade]:
|
|
1274
|
+
"""
|
|
1275
|
+
Creates Trade instances for each symbol provided.
|
|
1276
|
+
|
|
1277
|
+
Args:
|
|
1278
|
+
symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
|
|
1279
|
+
params: A dictionary containing parameters for the Trade instance.
|
|
1280
|
+
|
|
1281
|
+
Returns:
|
|
1282
|
+
A dictionary where keys are symbols and values are corresponding Trade instances.
|
|
1283
|
+
|
|
1284
|
+
Raises:
|
|
1285
|
+
ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
|
|
1286
|
+
"""
|
|
1287
|
+
instances = {}
|
|
1288
|
+
|
|
1289
|
+
if not symbols:
|
|
1290
|
+
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1291
|
+
for symbol in symbols:
|
|
1292
|
+
try:
|
|
1293
|
+
instances[symbol] = Trade(**params, symbol=symbol)
|
|
1294
|
+
except Exception as e:
|
|
1295
|
+
logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1296
|
+
return instances
|