bbstrader 0.3.0__py3-none-any.whl → 0.3.2__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/models/risk.py CHANGED
@@ -1,398 +1,28 @@
1
- import pickle
2
- from abc import ABCMeta, abstractmethod
3
- from datetime import datetime
4
- from typing import Dict, Optional
5
1
 
6
- import numpy as np
7
- import pandas as pd
8
- import seaborn as sns
9
- from hmmlearn.hmm import GaussianHMM
10
- from matplotlib import cm
11
- from matplotlib import pyplot as plt
12
- from matplotlib.dates import MonthLocator, YearLocator
13
2
 
14
- from bbstrader.metatrader.rates import Rates
15
-
16
- sns.set_theme()
17
-
18
- __all__ = ["RiskModel", "HMMRiskManager", "build_hmm_models"]
19
-
20
-
21
- class RiskModel(metaclass=ABCMeta):
22
- """
23
- The RiskModel class serves as an abstract base for implementing
24
- risk management strategies in financial markets. It is designed
25
- to assist in the decision-making process regarding which trades
26
- are permissible under current market conditions and how to allocate
27
- assets effectively to optimize the risk-reward ratio.
28
-
29
- Risk management is a critical component in trading and investment
30
- strategies, aiming to minimize potential losses without significantly
31
- reducing the potential for gains. This class encapsulates the core
32
- principles of risk management by providing a structured approach to
33
- evaluate market conditions and manage asset allocation.
34
-
35
- Implementing classes are required to define two key methods:
36
-
37
- - `which_trade_allowed`:
38
- Determines the types of trades that are permissible based on the
39
- analysis of current market conditions and the risk profile of the portfolio.
40
- This method should analyze the provided `returns_val` parameter,
41
- which could represent historical returns, volatility measures,
42
- or other financial metrics, to decide on the suitability of executing
43
- certain trades.
44
-
45
- - `which_quantity_allowed`:
46
- Defines how assets should be allocated across the portfolio to maintain
47
- an optimal balance between risk and return. This involves determining
48
- the quantity of each asset that can be held, considering factors such as
49
- diversification, liquidity, and the asset's volatility. This method ensures
50
- that the portfolio adheres to predefined risk tolerance levels and
51
- investment objectives.
52
-
53
- Note:
54
- Implementing these methods requires a deep understanding of risk
55
- management theories, market analysis, and portfolio management principles.
56
- The implementation should be tailored to the specific needs of the
57
- investment strategy and the risk tolerance of the investor or the fund
58
- being managed.
59
- """
60
-
61
- @abstractmethod
62
- def which_trade_allowed(self, returns_val):
63
- """
64
- Determines the types of trades permissible under current market conditions.
65
-
66
- Parameters:
67
- returns_val: A parameter representing financial metrics
68
- such as historical returns or volatility, used to
69
- assess market conditions.
70
- """
71
- pass
72
-
73
- @abstractmethod
74
- def which_quantity_allowed(self):
75
- """
76
- Defines the strategy for asset allocation within
77
- the portfolio to optimize risk-reward ratio.
78
- """
79
- pass
80
-
81
-
82
- class HMMRiskManager(RiskModel):
83
- """
84
- This class represents a risk management model using Hidden Markov Models (HMM)
85
- to identify and manage market risks. It inherits from a generic RiskModel class
86
- and utilizes Gaussian HMM to model the financial market's hidden states. These states
87
- are used to make decisions on permissible trading actions based on the identified market
88
- trends, thus facilitating a risk-aware trading strategy.
89
-
90
- To learn more about about the Hidden Markov model,
91
- See https://en.wikipedia.org/wiki/Hidden_Markov_model
92
-
93
- Exemple:
94
- >>> # Assuming `data` is a DataFrame containing your market data
95
- >>> risk_manager = HMMRiskManager(data=data, states=3, iterations=200, verbose=True)
96
- >>> current_regime = risk_manager.get_current_regime(data['Returns'].values)
97
- >>> print(f"Current Market Regime: {current_regime}")
98
-
99
- """
3
+ class HMMRiskManager():
100
4
 
101
5
  def __init__(
102
6
  self,
103
- data: Optional[pd.DataFrame] = None,
104
- states: int = 2,
105
- iterations: int = 100,
106
- end_date: Optional[datetime] = None,
107
- csv_filepath: Optional[str] = None,
7
+ data,
8
+ states,
9
+ iterations,
10
+ end_date,
11
+ csv_filepath,
108
12
  **kwargs,
109
13
  ):
110
- """
111
- Initializes the HMMRiskManager with market data and model parameters.
112
-
113
- Args:
114
- `data` : DataFrame containing market data.
115
- If not provided, data must be loaded via csv_filepath.
116
-
117
- `states` (int): The number of hidden states in the HMM.
118
- `iterations` (int): The number of iterations for the HMM to converge.
119
- `end_date` (datetime): The end date for the market data to be considered.
120
-
121
- `csv_filepath` (str): Path to the CSV file containing
122
- market data if data is not provided directly.
123
-
124
- kwarg (dict): Additional arguments
125
- - `model_filename` (str): Filename to save the trained HMM model.
126
- - `verbose` (bool): If True, prints additional model information.
127
-
128
- - `cov_variance` (str): Type of covariance to use in the HMM.
129
- possibles values are "spherical", "tied", "diag", "full".
130
- see https://hmmlearn.readthedocs.io/en/latest/api.html#gaussianhmm for more details.
131
-
132
- """
133
- self.data = data
134
- self.states = states
135
- self.iterations = iterations
136
- self.end_date = end_date
137
- self.csv_filepath = csv_filepath
138
-
139
- self.model_filename = kwargs.get("model_filename")
140
- self.verbose = kwargs.get("verbose", True)
141
- self.cov_variance = kwargs.get("cov_variance", "diag")
142
-
143
- self.df = self._get_data()
144
- self.hmm_model = self._fit_model()
145
- if self.verbose:
146
- self.show_hidden_states()
147
- trends = self.identify_market_trends()
148
- self.bullish_state = trends["bullish"]
149
- self.bearish_state = trends["bearish"]
150
- self.allowed_regimes = [s for s in trends.values()]
151
-
152
- def _get_data(self):
153
- """
154
- Retrieves market data for the model either
155
- from a provided DataFrame or a CSV file.
156
- """
157
- if self.data is not None:
158
- return self.data
159
- elif self.csv_filepath is not None:
160
- return self.read_csv_file(self.csv_filepath)
161
- else:
162
- raise ValueError("No data source provided.")
163
-
164
- def _fit_model(self):
165
- """
166
- Fits the HMM model to the market data
167
- and saves the model if a filename is provided.
168
- """
169
- df = self.df.copy()
170
- data = self.obtain_prices_df(df, end=self.end_date)
171
- returns = np.column_stack([data["Returns"]])
172
-
173
- hmm_model = GaussianHMM(
174
- n_components=self.states,
175
- covariance_type=self.cov_variance,
176
- n_iter=self.iterations,
177
- ).fit(returns)
14
+ import warnings
178
15
 
179
- if self.verbose:
180
- print(f"Hidden Markov model (HMM) Score: {hmm_model.score(returns)}")
181
- if self.model_filename is not None:
182
- self.save_hmm_model(hmm_model, self.model_filename)
183
-
184
- return hmm_model
185
-
186
- def get_states(self):
187
- """
188
- Predicts the hidden states for the market data
189
- and calculates the mean returns and volatility for each state.
190
-
191
- Returns:
192
- DataFrame containing mean returns
193
- and volatility for each hidden state.
194
- """
195
- df = self.df.copy()
196
- data = self.obtain_prices_df(df, end=self.end_date)
197
- returns = np.column_stack([data["Returns"]])
198
- states = self.hmm_model.predict(returns)
199
- data["State"] = states
200
- # Calculating mean and volatility for each state
201
- state_stats = (
202
- data.groupby("State")["Returns"]
203
- .agg(["mean", "std"])
204
- .rename(columns={"mean": "Mean Returns", "std": "Volatility"})
205
- )
206
- return state_stats
207
-
208
- def identify_market_trends(self):
209
- """
210
- Identifies bullish and bearish market trends
211
- based on the mean returns and volatility of each state.
212
-
213
- Returns:
214
- A dictionary with keys 'bullish' and 'bearish'
215
- indicating the identified states.
216
- """
217
- df = self.get_states()
218
- # Sort the df based on Mean Returns and then by lower Volatility
219
- sorted_df = df.sort_values(
220
- by=["Mean Returns", "Volatility"], ascending=[False, True]
221
- )
222
-
223
- # The first row after sorting will be the bullish state
224
- # (highest mean return, lower volatility preferred)
225
- bullish_state = sorted_df.index[0]
226
-
227
- # The last row will be the bearish state
228
- # (as it has the lowest mean return)
229
- bearish_state = sorted_df.index[-1]
230
-
231
- return {"bullish": bullish_state, "bearish": bearish_state}
232
-
233
- def get_current_regime(self, returns_val):
234
- """
235
- Determines the current market regime based on the latest returns.
236
-
237
- Args:
238
- returns_val : Array of recent return values
239
- to predict the current state.
240
-
241
- Returns:
242
- The predicted current market state.
243
- """
244
- returns = returns_val[~(np.isnan(returns_val) | np.isinf(returns_val))]
245
- features = np.array(returns).reshape(-1, 1)
246
- current_regime = self.hmm_model.predict(features)[-1]
247
- return current_regime
248
-
249
- def which_trade_allowed(self, returns_val):
250
- """
251
- Decides whether a long or short trade
252
- is allowed based on the current market regime.
253
- This method override the which_trade_allowed() from
254
- RiskModel class .
255
-
256
- Args:
257
- returns_val : Array of recent return values
258
- to assess the permissible trade.
259
-
260
- Returns:
261
- A string indicating "LONG" or "SHORT"
262
- if a trade is allowed, or None if no trade is permitted.
263
- """
264
- state = self.get_current_regime(returns_val)
265
- if state in self.allowed_regimes:
266
- trade = "LONG" if state == self.bullish_state else "SHORT"
267
- return trade
268
- else:
269
- return None
270
-
271
- def which_quantity_allowed(self): ...
272
-
273
- def save_hmm_model(self, hmm_model, filename):
274
- """
275
- Saves the trained HMM model to a pickle file.
276
-
277
- Args:
278
- hmm_model : The trained GaussianHMM model to be saved.
279
- filename : The filename under which to save the model.
280
- """
281
- print("Pickling HMM model...")
282
- pickle.dump(hmm_model, open(f"{filename}.pkl", "wb"))
283
- print("...HMM model pickled.")
284
-
285
- def read_csv_file(self, csv_filepath):
286
- """
287
- Reads market data from a CSV file.
288
-
289
- Args:
290
- csv_filepath : Path to the CSV file containing the market data.
291
-
292
- Returns:
293
- DataFrame containing the parsed market data.
294
- """
295
- df = pd.read_csv(
296
- csv_filepath,
297
- header=0,
298
- names=["Date", "Open", "High", "Low", "Close", "Adj Close", "Volume"],
299
- index_col="Date",
300
- parse_dates=True,
16
+ warnings.warn(
17
+ "The class 'HMMRiskManager' is deprecated. ",
18
+ DeprecationWarning,
301
19
  )
302
- return df
303
-
304
- def obtain_prices_df(self, data_frame, end=None):
305
- """
306
- Processes the market data to calculate returns
307
- and optionally filters data up to a specified end date.
308
-
309
- Args:
310
- data_frame : DataFrame containing the market data.
311
- end : Optional datetime object specifying the end date for the data.
312
-
313
- Returns:
314
- DataFrame with returns calculated
315
- and data filtered up to the end date if specified.
316
- """
317
- df = data_frame.copy()
318
- if "Returns" or "returns" not in df.columns:
319
- if "Close" in df.columns:
320
- df["Returns"] = df["Close"].pct_change()
321
- elif "Adj Close" in df.columns:
322
- df["Returns"] = df["Adj Close"].pct_change()
323
- else:
324
- raise ValueError("No 'Close' or 'Adj Close' columns found.")
325
- elif "returns" in df.columns:
326
- df.rename(columns={"returns": "Returns"}, inplace=True)
327
- if end is not None:
328
- df = df[: end.strftime("%Y-%m-%d")]
329
- df.dropna(inplace=True)
330
- return df
331
-
332
- def show_hidden_states(self):
333
- """
334
- Visualizes the market data
335
- and the predicted hidden states from the HMM model.
336
- """
337
- data = self.df.copy()
338
- df = self.obtain_prices_df(data, end=self.end_date)
339
- self.plot_hidden_states(self.hmm_model, df)
340
-
341
- def plot_hidden_states(self, hmm_model, df):
342
- """
343
- Plots the adjusted closing prices masked
344
- by the in-sample hidden states as a mechanism
345
- to understand the market regimes.
346
-
347
- Args:
348
- hmm_model : The trained GaussianHMM model used for predicting states.
349
- df : DataFrame containing the market data with calculated returns.
350
- """
351
- hidden_states = hmm_model.predict(df[["Returns"]])
352
- fig, axs = plt.subplots(hmm_model.n_components, sharex=True, sharey=True)
353
- colours = cm.rainbow(np.linspace(0, 1, hmm_model.n_components))
354
20
 
355
- for i, (ax, colour) in enumerate(zip(axs, colours)):
356
- mask = hidden_states == i
357
- ax.plot_date(
358
- df.index[mask], df["Adj Close"][mask], ".", linestyle="none", c=colour
359
- )
360
- ax.set_title(f"Hidden State #{i}")
361
- ax.xaxis.set_major_locator(YearLocator())
362
- ax.xaxis.set_minor_locator(MonthLocator())
363
- ax.grid(True)
364
- plt.show()
365
21
 
22
+ def build_hmm_models(symbol_list=None, **kwargs):
23
+ import warnings
366
24
 
367
- def build_hmm_models(symbol_list=None, **kwargs) -> Dict[str, HMMRiskManager]:
368
- mt5_data = kwargs.get("use_mt5_data", False)
369
- data = kwargs.get("hmm_data")
370
- tf = kwargs.get("time_frame", "D1")
371
- hmm_end = kwargs.get("hmm_end", 0)
372
- sd = kwargs.get("session_duration", 23.0)
373
- hmm_tickers = kwargs.get("hmm_tickers")
374
- if hmm_tickers is not None:
375
- symbols = hmm_tickers
376
- else:
377
- symbols = symbol_list
378
- hmm_models = {symbol: None for symbol in symbols}
379
- if data is not None:
380
- if isinstance(data, pd.DataFrame):
381
- hmm_data = data
382
- hmm = HMMRiskManager(data=hmm_data, verbose=True, iterations=1000, **kwargs)
383
- for symbol in symbols:
384
- hmm_models[symbol] = hmm
385
- elif isinstance(data, dict):
386
- for symbol, data in data.items():
387
- hmm = HMMRiskManager(data=data, verbose=True, iterations=1000, **kwargs)
388
- hmm_models[symbol] = hmm
389
- if mt5_data:
390
- for symbol in symbols:
391
- rates = Rates(
392
- symbol, timeframe=tf, start_pos=hmm_end, session_duration=sd, **kwargs
393
- )
394
- data = rates.get_rates_from_pos()
395
- assert data is not None, f"No data for {symbol}"
396
- hmm = HMMRiskManager(data=data, verbose=True, iterations=1000, **kwargs)
397
- hmm_models[symbol] = hmm
398
- return hmm_models
25
+ warnings.warn(
26
+ "The function 'build_hmm_models' is deprecated.",
27
+ DeprecationWarning,
28
+ )
@@ -1,14 +1,15 @@
1
1
  import multiprocessing as mp
2
+ import sys
2
3
  import time
3
4
  from datetime import date, datetime
4
- from typing import Dict, List, Literal, Optional
5
+ from typing import Callable, Dict, List, Literal, Optional
5
6
 
6
7
  import pandas as pd
7
8
  from loguru import logger as log
8
9
 
9
10
  from bbstrader.btengine.strategy import MT5Strategy, Strategy
10
11
  from bbstrader.config import BBSTRADER_DIR
11
- from bbstrader.metatrader.account import Account, check_mt5_connection
12
+ from bbstrader.metatrader.account import check_mt5_connection
12
13
  from bbstrader.metatrader.trade import Trade, TradeAction, TradingMode
13
14
  from bbstrader.trading.utils import send_message
14
15
 
@@ -36,6 +37,8 @@ _TF_MAPPING = {
36
37
  "D1": 1440,
37
38
  }
38
39
 
40
+ MT5_ENGINE_TIMEFRAMES = list(_TF_MAPPING.keys())
41
+
39
42
  TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
40
43
  WEEK_DAYS = TradingDays + ["saturday", "sunday"]
41
44
  FRIDAY = "friday"
@@ -88,7 +91,7 @@ NON_EXEC_RETCODES = {
88
91
  log.add(
89
92
  f"{BBSTRADER_DIR}/logs/execution.log",
90
93
  enqueue=True,
91
- level="INFO",
94
+ level="DEBUG",
92
95
  format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
93
96
  )
94
97
 
@@ -171,10 +174,11 @@ class Mt5ExecutionEngine:
171
174
  self,
172
175
  symbol_list: List[str],
173
176
  trades_instances: Dict[str, Trade],
174
- strategy_cls: Strategy,
177
+ strategy_cls: Strategy | MT5Strategy,
175
178
  /,
176
179
  mm: bool = True,
177
180
  auto_trade: bool = True,
181
+ prompt_callback: Callable = None,
178
182
  optimizer: str = "equal",
179
183
  trail: bool = True,
180
184
  stop_trail: Optional[int] = None,
@@ -201,6 +205,8 @@ class Mt5ExecutionEngine:
201
205
  auto_trade : If set to true, when signal are generated by the strategy class,
202
206
  the Execution engine will automaticaly open position in other whise it will prompt
203
207
  the user for confimation.
208
+ prompt_callback : Callback function to prompt the user for confirmation.
209
+ This is useful when integrating with GUI applications.
204
210
  show_positions_orders : Print open positions and orders. Defaults to False.
205
211
  iter_time : Interval to check for signals and `mm`. Defaults to 5.
206
212
  use_trade_time : Open trades after the time is completed. Defaults to True.
@@ -244,6 +250,7 @@ class Mt5ExecutionEngine:
244
250
  self.strategy_cls = strategy_cls
245
251
  self.mm = mm
246
252
  self.auto_trade = auto_trade
253
+ self.prompt_callback = prompt_callback
247
254
  self.optimizer = optimizer
248
255
  self.trail = trail
249
256
  self.stop_trail = stop_trail
@@ -267,6 +274,7 @@ class Mt5ExecutionEngine:
267
274
 
268
275
  self._initialize_engine(**kwargs)
269
276
  self.strategy = self._init_strategy(**kwargs)
277
+ self._running = True
270
278
 
271
279
  def __repr__(self):
272
280
  trades = self.trades_instances.keys()
@@ -308,16 +316,16 @@ class Mt5ExecutionEngine:
308
316
  def _print_exc(self, msg: str, e: Exception):
309
317
  if isinstance(e, KeyboardInterrupt):
310
318
  logger.info("Stopping the Execution Engine ...")
311
- quit()
319
+ sys.exit(0)
312
320
  if self.debug_mode:
313
321
  raise ValueError(msg).with_traceback(e.__traceback__)
314
322
  else:
315
- logger.error(f"{msg, repr(e)}")
323
+ logger.error(f"{msg}: {type(e).__name__}: {str(e)}")
316
324
 
317
325
  def _max_trades(self, mtrades):
318
326
  max_trades = {
319
327
  symbol: mtrades[symbol]
320
- if mtrades is not None and symbol in mtrades
328
+ if mtrades is not None and isinstance(mtrades, dict) and symbol in mtrades
321
329
  else self.trades_instances[symbol].max_trade()
322
330
  for symbol in self.symbols
323
331
  }
@@ -332,10 +340,10 @@ class Mt5ExecutionEngine:
332
340
  expert_ids = [expert_ids]
333
341
  return expert_ids
334
342
 
335
- def _init_strategy(self, **kwargs):
343
+ def _init_strategy(self, **kwargs) -> MT5Strategy:
336
344
  try:
337
345
  check_mt5_connection(**kwargs)
338
- strategy: MT5Strategy = self.strategy_cls(
346
+ strategy = self.strategy_cls(
339
347
  symbol_list=self.symbols, mode=TradingMode.LIVE, **kwargs
340
348
  )
341
349
  except Exception as e:
@@ -350,7 +358,7 @@ class Mt5ExecutionEngine:
350
358
  return strategy
351
359
 
352
360
  def _get_signal_info(self, signal, symbol, price, stoplimit):
353
- account = Account(**self.kwargs)
361
+ account = self.strategy.account
354
362
  symbol_info = account.get_symbol_info(symbol)
355
363
 
356
364
  common_data = {
@@ -367,21 +375,21 @@ class Mt5ExecutionEngine:
367
375
  ).format(**common_data)
368
376
 
369
377
  sigmsg = (
370
- "SIGNAL={signal},\n"
371
- "SYMBOL={symbol},\n"
372
- "TYPE={symbol_type},\n"
373
- "DESCRIPTION={description},\n"
374
- "PRICE={price},\n"
375
- "STOPLIMIT={stoplimit},\n"
376
- "STRATEGY={strategy},\n"
377
- "TIMEFRAME={timeframe},\n"
378
- "BROKER={broker},\n"
378
+ "SIGNAL={signal}\n"
379
+ "SYMBOL={symbol}\n"
380
+ "TYPE={symbol_type}\n"
381
+ "DESCRIPTION={description}\n"
382
+ "PRICE={price}\n"
383
+ "STOPLIMIT={stoplimit}\n"
384
+ "STRATEGY={strategy}\n"
385
+ "TIMEFRAME={timeframe}\n"
386
+ "BROKER={broker}\n"
379
387
  "TIMESTAMP={timestamp}"
380
388
  ).format(
381
389
  **common_data,
382
390
  symbol_type=account.get_symbol_type(symbol).value,
383
- description=symbol_info.description,
384
- price=price,
391
+ description=symbol_info.description if symbol_info else "N/A",
392
+ price=price if price else "MARKET",
385
393
  stoplimit=stoplimit,
386
394
  broker=account.broker.name,
387
395
  timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
@@ -506,6 +514,9 @@ class Mt5ExecutionEngine:
506
514
  or (period_type == "day" and closing)
507
515
  or (period_type == "24/7" and closing)
508
516
  ):
517
+ logger.info(
518
+ f"{self.ACCOUNT} Closing all positions and orders for {symbol} ..."
519
+ )
509
520
  for id in self.expert_ids:
510
521
  trade.close_positions(
511
522
  position_type="all", id=id, comment=self.comment
@@ -520,7 +531,7 @@ class Mt5ExecutionEngine:
520
531
  def _daily_end_checks(self, today, closing, sessionmsg):
521
532
  self.strategy.perform_period_end_checks()
522
533
  if self.period_end_action == "break" and closing:
523
- exit(0)
534
+ sys.exit(0)
524
535
  elif self.period_end_action == "sleep" and today != FRIDAY or not closing:
525
536
  self._sleep_over_night(sessionmsg)
526
537
  elif self.period_end_action == "sleep" and today == FRIDAY:
@@ -532,7 +543,7 @@ class Mt5ExecutionEngine:
532
543
  elif today == FRIDAY:
533
544
  self.strategy.perform_period_end_checks()
534
545
  if self.period_end_action == "break" and closing:
535
- exit(0)
546
+ sys.exit(0)
536
547
  elif self.period_end_action == "sleep" or not closing:
537
548
  self._sleep_over_weekend(sessionmsg)
538
549
 
@@ -541,7 +552,7 @@ class Mt5ExecutionEngine:
541
552
  self._sleep_over_night(sessionmsg)
542
553
  elif today == FRIDAY and self._is_month_ends() and closing:
543
554
  self.strategy.perform_period_end_checks()
544
- exit(0)
555
+ sys.exit(0)
545
556
  else:
546
557
  self._sleep_over_weekend(sessionmsg)
547
558
 
@@ -621,17 +632,32 @@ class Mt5ExecutionEngine:
621
632
  )
622
633
  pass
623
634
 
624
- def _open_buy(
625
- self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
626
- ):
635
+ def _auto_trade(self, sigmsg, symbol) -> bool:
627
636
  if self.notify:
628
637
  self._send_notification(sigmsg, symbol)
638
+ if self.auto_trade:
639
+ return True
629
640
  if not self.auto_trade:
630
- auto_trade = input(
631
- f"{sigmsg} \n\n Please enter Y/Yes to accept this order or N/No to reject it :"
641
+ prompt = (
642
+ f"{sigmsg} \n Enter Y/Yes to accept or N/No to reject this order : "
632
643
  )
644
+ if self.prompt_callback is not None:
645
+ auto_trade = self.prompt_callback(prompt)
646
+ else:
647
+ auto_trade = input(prompt)
633
648
  if not auto_trade.upper().startswith("Y"):
634
- return
649
+ info = f"Order Rejected !!! SYMBOL={symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
650
+ logger.info(info)
651
+ if self.notify:
652
+ self._send_notification(info, symbol)
653
+ return False
654
+ return True
655
+
656
+ def _open_buy(
657
+ self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
658
+ ):
659
+ if not self._auto_trade(sigmsg, symbol):
660
+ return
635
661
  if not self._check_retcode(trade, "BMKT"):
636
662
  logger.info(msg)
637
663
  trade.open_buy_position(
@@ -647,14 +673,8 @@ class Mt5ExecutionEngine:
647
673
  def _open_sell(
648
674
  self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
649
675
  ):
650
- if self.notify:
651
- self._send_notification(sigmsg, symbol)
652
- if not self.auto_trade:
653
- auto_trade = input(
654
- f"{sigmsg} \n\n Please enter Y/Yes to accept this order or N/No to reject it :"
655
- )
656
- if not auto_trade.upper().startswith("Y"):
657
- return
676
+ if not self._auto_trade(sigmsg, symbol):
677
+ return
658
678
  if not self._check_retcode(trade, "SMKT"):
659
679
  logger.info(msg)
660
680
  trade.open_sell_position(
@@ -905,7 +925,7 @@ class Mt5ExecutionEngine:
905
925
  pass
906
926
 
907
927
  def run(self):
908
- while True:
928
+ while self._running:
909
929
  try:
910
930
  check_mt5_connection(**self.kwargs)
911
931
  positions_orders = self._check_positions_orders()
@@ -927,12 +947,21 @@ class Mt5ExecutionEngine:
927
947
  self._sleep()
928
948
  self._handle_period_end_actions(today)
929
949
  except KeyboardInterrupt:
930
- logger.info("Stopping the Execution Engine ...")
950
+ logger.info(
951
+ f"Stopping Execution Engine for {self.STRATEGY} STRATEGY on {self.ACCOUNT} Account"
952
+ )
931
953
  break
932
954
  except Exception as e:
933
955
  msg = f"Running Execution Engine, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
934
956
  self._print_exc(msg, e)
935
- continue
957
+
958
+ def stop(self):
959
+ """Stops the execution engine."""
960
+ self._running = False
961
+ logger.info(
962
+ f"Stopping Execution Engine for {self.STRATEGY} STRATEGY on {self.ACCOUNT} Account"
963
+ )
964
+ logger.info("Execution Engine stopped successfully.")
936
965
 
937
966
 
938
967
  def RunMt5Engine(account_id: str, **kwargs):