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.

Files changed (36) hide show
  1. bbstrader/__ini__.py +20 -20
  2. bbstrader/__main__.py +50 -50
  3. bbstrader/btengine/__init__.py +54 -54
  4. bbstrader/btengine/data.py +11 -9
  5. bbstrader/btengine/scripts.py +157 -157
  6. bbstrader/compat.py +19 -19
  7. bbstrader/config.py +137 -137
  8. bbstrader/core/data.py +22 -22
  9. bbstrader/core/utils.py +146 -146
  10. bbstrader/metatrader/__init__.py +6 -6
  11. bbstrader/metatrader/account.py +1516 -1516
  12. bbstrader/metatrader/copier.py +750 -735
  13. bbstrader/metatrader/rates.py +584 -584
  14. bbstrader/metatrader/risk.py +749 -748
  15. bbstrader/metatrader/scripts.py +81 -81
  16. bbstrader/metatrader/trade.py +1836 -1826
  17. bbstrader/metatrader/utils.py +645 -645
  18. bbstrader/models/__init__.py +10 -10
  19. bbstrader/models/factors.py +312 -312
  20. bbstrader/models/ml.py +1272 -1265
  21. bbstrader/models/optimization.py +182 -182
  22. bbstrader/models/portfolio.py +223 -223
  23. bbstrader/models/risk.py +398 -398
  24. bbstrader/trading/__init__.py +11 -11
  25. bbstrader/trading/execution.py +846 -842
  26. bbstrader/trading/script.py +155 -155
  27. bbstrader/trading/scripts.py +69 -69
  28. bbstrader/trading/strategies.py +860 -860
  29. bbstrader/tseries.py +1842 -1842
  30. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/LICENSE +21 -21
  31. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/METADATA +188 -187
  32. bbstrader-0.2.94.dist-info/RECORD +44 -0
  33. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/WHEEL +1 -1
  34. bbstrader-0.2.92.dist-info/RECORD +0 -44
  35. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/entry_points.txt +0 -0
  36. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/top_level.txt +0 -0
@@ -1,748 +1,749 @@
1
- import random
2
- from datetime import datetime
3
- from typing import Any, Dict, Optional, Union
4
-
5
- from scipy.stats import norm
6
-
7
- from bbstrader.metatrader.account import Account
8
- from bbstrader.metatrader.rates import Rates
9
- from bbstrader.metatrader.utils import TIMEFRAMES, TimeFrame
10
-
11
- try:
12
- import MetaTrader5 as Mt5
13
- except ImportError:
14
- import bbstrader.compat # noqa: F401
15
-
16
-
17
- _COMMD_SUPPORTED_ = [
18
- "GOLD",
19
- "SILVER",
20
- "BRENT",
21
- "CRUDOIL",
22
- "WTI",
23
- "UKOIL",
24
- "XAGEUR",
25
- "XAGUSD",
26
- "XAGAUD",
27
- "XAGGBP",
28
- "XAUAUD",
29
- "XAUEUR",
30
- "XAUUSD",
31
- "XAUGBP",
32
- "USOIL",
33
- "SpotCrude",
34
- "SpotBrent",
35
- "Soybeans",
36
- "Wheat",
37
- "SoyOil",
38
- "LeanHogs",
39
- "LDSugar",
40
- "Coffee",
41
- "OJ",
42
- "Cocoa",
43
- "Cattle",
44
- "Copper",
45
- "XCUUSD",
46
- "NatGas",
47
- "Gasoline",
48
- ]
49
-
50
- _ADMIRAL_MARKETS_FUTURES_ = [
51
- "#USTNote_",
52
- "#Bund_",
53
- "#USDX_",
54
- "_AUS200_",
55
- "_Canada60_",
56
- "_SouthAfrica40_",
57
- "_STXE600_",
58
- "_EURO50_",
59
- "_GER40_",
60
- "_GermanyTech30_",
61
- "_MidCapGER50_",
62
- "_SWISS20_",
63
- "_UK100_",
64
- "_USNASDAQ100_",
65
- "_YM_",
66
- "_ES_",
67
- "_CrudeOilUS_",
68
- "_DUTCH25_",
69
- "_FRANCE40_",
70
- "_NORWAY25_",
71
- "_SPAIN35_",
72
- "_CrudeOilUK_",
73
- "_XAU_",
74
- "_HK50_",
75
- "_HSCEI50_",
76
- ]
77
-
78
- __PEPPERSTONE_FUTURES__ = [
79
- "AUS200-F",
80
- "GER40-F",
81
- "HK50-F",
82
- "JPN225-F",
83
- "UK100-F",
84
- "US30-F",
85
- "NAS100-F",
86
- "US500-F",
87
- "Crude-F",
88
- "Brent-F",
89
- "XAUUSD-F",
90
- "XAGUSD-F",
91
- "USDX-F",
92
- "EUSTX50-F",
93
- "FRA40-F",
94
- "GERTEC30-F",
95
- "SPA35-F",
96
- "SWI20-F",
97
- ]
98
-
99
- __all__ = ["RiskManagement"]
100
-
101
-
102
- class RiskManagement(Account):
103
- """
104
- The RiskManagement class provides foundational
105
- risk management functionalities for trading activities.
106
- It calculates risk levels, determines stop loss and take profit levels,
107
- and ensures trading activities align with predefined risk parameters.
108
-
109
- Exemple:
110
- >>> risk_manager = RiskManagement(
111
- ... symbol="EURUSD",
112
- ... max_risk=5.0,
113
- ... daily_risk=2.0,
114
- ... max_trades=10,
115
- ... std_stop=True,
116
- ... account_leverage=True,
117
- ... start_time="09:00",
118
- ... finishing_time="17:00",
119
- ... time_frame="1h"
120
- ... )
121
- >>> # Calculate risk level
122
- >>> risk_level = risk_manager.risk_level()
123
-
124
- >>> # Get appropriate lot size for a trade
125
- >>> lot_size = risk_manager.get_lot()
126
-
127
- >>> # Determine stop loss and take profit levels
128
- >>> stop_loss = risk_manager.get_stop_loss()
129
- >>> take_profit = risk_manager.get_take_profit()
130
-
131
- >>> # Check if current risk is acceptable
132
- >>> is_risk_acceptable = risk_manager.is_risk_ok()
133
- """
134
-
135
- def __init__(
136
- self,
137
- symbol: str,
138
- max_risk: float = 10.0,
139
- daily_risk: Optional[float] = None,
140
- max_trades: Optional[int] = None,
141
- std_stop: bool = False,
142
- pchange_sl: Optional[float] = None,
143
- var_level: float = 0.95,
144
- var_time_frame: TimeFrame = "D1",
145
- account_leverage: bool = True,
146
- time_frame: TimeFrame = "D1",
147
- start_time: str = "1:00",
148
- finishing_time: str = "23:00",
149
- sl: Optional[int] = None,
150
- tp: Optional[int] = None,
151
- be: Optional[int] = None,
152
- rr: float = 1.5,
153
- **kwargs,
154
- ):
155
- """
156
- Initialize the RiskManagement class to manage risk in trading activities.
157
-
158
- Args:
159
- symbol (str): The symbol of the financial instrument to trade.
160
- max_risk (float): The `maximum risk allowed` on the trading account.
161
- daily_risk (float, optional): `Daily Max risk allowed`.
162
- If Set to None it will be determine based on Maximum risk.
163
- max_trades (int, optional): Maximum number of trades in a trading session.
164
- If set to None it will be determine based on the timeframe of trading.
165
- std_stop (bool, optional): If set to True, the Stop loss is calculated based
166
- On `historical volatility` of the trading instrument. Defaults to False.
167
- pchange_sl (float, optional): If set, the Stop loss is calculated based
168
- On `percentage change` of the trading instrument.
169
- var_level (float, optional): Confidence level for Value-at-Risk,e.g., 0.99 for 99% confidence interval.
170
- The default is 0.95.
171
- var_time_frame (str, optional): Time frame to use to calculate the VaR.
172
- account_leverage (bool, optional): If set to True the account leverage will be used
173
- In risk management setting. Defaults to False.
174
- time_frame (str, optional): The time frame on which the program is working
175
- `(1m, 3m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, D1)`. Defaults to 'D1'.
176
- start_time (str, optional): The starting time for the trading strategy
177
- `(HH:MM, H an M do not star with 0)`. Defaults to "6:30".
178
- finishing_time (str, optional): The finishing time for the trading strategy
179
- `(HH:MM, H an M do not star with 0)`. Defaults to "19:30".
180
- sl (int, optional): Stop Loss in points, Must be a positive number.
181
- tp (int, optional): Take Profit in points, Must be a positive number.
182
- be (int, optional): Break Even in points, Must be a positive number.
183
- rr (float, optional): Risk reward ratio, Must be a positive number. Defaults to 1.5.
184
-
185
- See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
186
- """
187
- super().__init__(**kwargs)
188
-
189
- # Validation
190
- if daily_risk is not None and daily_risk < 0:
191
- raise ValueError("daily_risk must be a positive number")
192
- if max_risk <= 0:
193
- raise ValueError("max_risk must be a positive number")
194
- if sl is not None and not isinstance(sl, int):
195
- raise ValueError("sl must be an integer number")
196
- if tp is not None and not isinstance(tp, int):
197
- raise ValueError("tp must be an integer number")
198
- if be is not None and (not isinstance(be, int) or be <= 0):
199
- raise ValueError("be must be a positive integer number")
200
- if time_frame not in TIMEFRAMES:
201
- raise ValueError("Unsupported time frame {}".format(time_frame))
202
- if var_time_frame not in TIMEFRAMES:
203
- raise ValueError("Unsupported time frame {}".format(var_time_frame))
204
-
205
- self.kwargs = kwargs
206
- self.symbol = symbol
207
- self.start_time = start_time
208
- self.finishing_time = finishing_time
209
- self.max_trades = max_trades
210
- self.std = std_stop
211
- self.pchange = pchange_sl
212
- self.var_level = var_level
213
- self.var_tf = var_time_frame
214
- self.daily_dd = round(daily_risk, 5) if daily_risk is not None else None
215
- self.max_risk = max_risk
216
- self.rr = rr
217
- self.sl = sl
218
- self.tp = tp
219
- self.be = be
220
-
221
- self.account_leverage = account_leverage
222
- self.symbol_info = super().get_symbol_info(self.symbol)
223
-
224
- self._tf = time_frame
225
-
226
- @property
227
- def dailydd(self) -> float:
228
- return self.daily_dd
229
-
230
- @dailydd.setter
231
- def dailydd(self, value: float):
232
- self.daily_dd = value
233
-
234
- @property
235
- def maxrisk(self) -> float:
236
- return self.max_risk
237
-
238
- @maxrisk.setter
239
- def maxrisk(self, value: float):
240
- self.max_risk = value
241
-
242
- def _convert_time_frame(self, tf: str) -> int:
243
- """Convert time frame to minutes"""
244
- if tf == "D1":
245
- tf_int = self.get_minutes()
246
- elif "m" in tf:
247
- tf_int = TIMEFRAMES[tf]
248
- elif "h" in tf:
249
- tf_int = int(tf[0]) * 60
250
- elif tf == "W1":
251
- tf_int = self.get_minutes() * 5
252
- elif tf == "MN1":
253
- tf_int = self.get_minutes() * 22
254
- return tf_int
255
-
256
- def risk_level(self) -> float:
257
- """
258
- Calculates the risk level of a trade
259
-
260
- Returns:
261
- - Risk level in the form of a float percentage.
262
- """
263
- account_info = self.get_account_info()
264
- balance = account_info.balance
265
- equity = account_info.equity
266
- df = self.get_trades_history()
267
- if df is None:
268
- profit = 0
269
- else:
270
- profit_df = df.iloc[1:]
271
- profit = profit_df["profit"].sum()
272
- commisions = df["commission"].sum()
273
- fees = df["fee"].sum()
274
- swap = df["swap"].sum()
275
- total_profit = commisions + fees + swap + profit
276
- initial_balance = balance - total_profit
277
- if balance != 0:
278
- risk_alowed = (((equity - initial_balance) / equity) * 100) * -1
279
- return round(risk_alowed, 2)
280
- return 0.0
281
-
282
- def get_lot(self) -> float:
283
- """ "Get the approprite lot size for a trade"""
284
- s_info = self.symbol_info
285
- volume_step = s_info.volume_step
286
- lot = self.currency_risk()["lot"]
287
- steps = self._volume_step(volume_step)
288
- if float(steps) >= float(1):
289
- return round(lot, steps)
290
- else:
291
- return round(lot)
292
-
293
- def _volume_step(self, value):
294
- """Get the number of decimal places in a number"""
295
-
296
- value_str = str(value)
297
-
298
- if "." in value_str and value_str != "1.0":
299
- decimal_index = value_str.index(".")
300
- num_digits = len(value_str) - decimal_index - 1
301
- return num_digits
302
-
303
- elif value_str == "1.0":
304
- return 0
305
- else:
306
- return 0
307
-
308
- def max_trade(self) -> int:
309
- """calculates the maximum number of trades allowed"""
310
- minutes = self.get_minutes()
311
- tf_int = self._convert_time_frame(self._tf)
312
- if self.max_trades is not None:
313
- max_trades = self.max_trades
314
- else:
315
- max_trades = round(minutes / tf_int)
316
- return max(max_trades, 1)
317
-
318
- def get_minutes(self) -> int:
319
- """calculates the number of minutes between two times"""
320
-
321
- start = datetime.strptime(self.start_time, "%H:%M")
322
- end = datetime.strptime(self.finishing_time, "%H:%M")
323
- return (end - start).total_seconds() // 60
324
-
325
- def get_hours(self) -> int:
326
- """Calculates the number of hours between two times"""
327
-
328
- start = datetime.strptime(self.start_time, "%H:%M")
329
- end = datetime.strptime(self.finishing_time, "%H:%M")
330
- # Calculate the difference in hours
331
- hours = (end - start).total_seconds() // 3600 # 1 hour = 3600 seconds
332
-
333
- return hours
334
-
335
- def get_std_stop(self):
336
- """
337
- Calculate the standard deviation-based stop loss level
338
- for a given financial instrument.
339
-
340
- Returns:
341
- - Standard deviation-based stop loss level, rounded to the nearest point.
342
- - 0 if the calculated stop loss is less than or equal to 0.
343
- """
344
- minutes = self.get_minutes()
345
- tf_int = self._convert_time_frame(self._tf)
346
- interval = round((minutes / tf_int) * 252)
347
-
348
- rate = Rates(
349
- self.symbol, timeframe=self._tf, start_pos=0, count=interval, **self.kwargs
350
- )
351
- returns = rate.returns * 100
352
- std = returns.std()
353
- point = self.get_symbol_info(self.symbol).point
354
- av_price = (self.symbol_info.bid + self.symbol_info.ask) / 2
355
- price_interval = av_price * (100 - std) / 100
356
- sl_point = float((av_price - price_interval) / point)
357
- sl = round(sl_point)
358
- min_sl = self.symbol_info.trade_stops_level * 2 + self.get_deviation()
359
-
360
- return max(sl, min_sl)
361
-
362
- def get_pchange_stop(self, pchange: Optional[float]):
363
- """
364
- Calculate the percentage change-based stop loss level
365
- for a given financial instrument.
366
-
367
- Args:
368
- pchange (float): Percentage change in price to use for calculating stop loss level.
369
- If pchange is set to None, the stop loss is calculate using std.
370
-
371
- Returns:
372
- - Percentage change-based stop loss level, rounded to the nearest point.
373
- - 0 if the calculated stop loss is <= 0.
374
- """
375
- if pchange is not None:
376
- av_price = (self.symbol_info.bid + self.symbol_info.ask) / 2
377
- price_interval = av_price * (100 - pchange) / 100
378
- point = self.get_symbol_info(self.symbol).point
379
- sl_point = float((av_price - price_interval) / point)
380
- sl = round(sl_point)
381
- min_sl = self.symbol_info.trade_stops_level * 2 + self.get_deviation()
382
- return max(sl, min_sl)
383
- else:
384
- # Use std as default pchange
385
- return self.get_std_stop()
386
-
387
- def calculate_var(self, tf: TimeFrame = "D1", c=0.95):
388
- """
389
- Calculate Value at Risk (VaR) for a given portfolio.
390
-
391
- Args:
392
- tf (str): Time frame to use to calculate volatility.
393
- c (float): Confidence level for VaR calculation (default is 95%).
394
-
395
- Returns:
396
- - VaR value
397
- """
398
- minutes = self.get_minutes()
399
- tf_int = self._convert_time_frame(tf)
400
- interval = round((minutes / tf_int) * 252)
401
-
402
- rate = Rates(
403
- self.symbol, timeframe=tf, start_pos=0, count=interval, **self.kwargs
404
- )
405
- returns = rate.returns * 100
406
- p = self.get_account_info().margin_free
407
- mu = returns.mean()
408
- sigma = returns.std()
409
- var = self.var_cov_var(p, c, mu, sigma)
410
- return var
411
-
412
- def var_cov_var(self, P: float, c: float, mu: float, sigma: float):
413
- """
414
- Variance-Covariance calculation of daily Value-at-Risk.
415
-
416
- Args:
417
- P (float): Portfolio value in USD.
418
- c (float): Confidence level for Value-at-Risk,e.g., 0.99 for 99% confidence interval.
419
- mu (float): Mean of the returns of the portfolio.
420
- sigma (float): Standard deviation of the returns of the portfolio.
421
-
422
- Returns:
423
- - float: Value-at-Risk for the given portfolio.
424
- """
425
- alpha = norm.ppf(1 - c, mu, sigma)
426
- return P - P * (alpha + 1)
427
-
428
- def var_loss_value(self):
429
- """
430
- Calculate the stop-loss level based on VaR.
431
-
432
- Notes:
433
- The Var is Estimated using the Variance-Covariance method on the daily returns.
434
- If you want to use the VaR for a different time frame .
435
- """
436
- P = self.get_account_info().margin_free
437
- trade_risk = self.get_trade_risk()
438
- loss_allowed = P * trade_risk / 100
439
- var = self.calculate_var(c=self.var_level, tf=self.var_tf)
440
- return min(var, loss_allowed)
441
-
442
- def get_take_profit(self) -> int:
443
- """calculates the take profit of a trade in points"""
444
- deviation = self.get_deviation()
445
- if self.tp is not None:
446
- return self.tp + deviation
447
- else:
448
- return round(self.get_stop_loss() * self.rr)
449
-
450
- def get_stop_loss(self) -> int:
451
- """calculates the stop loss of a trade in points"""
452
- min_sl = self.symbol_info.trade_stops_level * 2 + self.get_deviation()
453
- if self.sl is not None:
454
- return max(self.sl, min_sl)
455
- elif self.sl is None and self.std:
456
- sl = self.get_std_stop()
457
- return max(sl, min_sl)
458
- elif self.sl is None and not self.std:
459
- risk = self.currency_risk()
460
- if risk["trade_loss"] != 0:
461
- sl = round((risk["currency_risk"] / risk["trade_loss"]))
462
- return max(sl, min_sl)
463
- return min_sl
464
-
465
- def get_currency_risk(self) -> float:
466
- """calculates the currency risk of a trade"""
467
- return round(self.currency_risk()["currency_risk"], 2)
468
-
469
- def expected_profit(self):
470
- """Calculate the expected profit per trade"""
471
- risk = self.get_currency_risk()
472
- return round(risk * self.rr, 2)
473
-
474
- def volume(self):
475
- """Volume per trade"""
476
-
477
- return self.currency_risk()["volume"]
478
-
479
- def currency_risk(self) -> Dict[str, Union[int, float, Any]]:
480
- """
481
- Calculates the currency risk of a trade.
482
-
483
- Returns:
484
- Dict[str, Union[int, float, Any]]: A dictionary containing the following keys:
485
-
486
- - `'currency_risk'`: Dollar amount risk on a single trade.
487
- - `'trade_loss'`: Loss value per tick in dollars.
488
- - `'trade_profit'`: Profit value per tick in dollars.
489
- - `'volume'`: Contract size multiplied by the average price.
490
- - `'lot'`: Lot size per trade.
491
- """
492
- s_info = self.symbol_info
493
-
494
- laverage = self.get_leverage(self.account_leverage)
495
- contract_size = s_info.trade_contract_size
496
-
497
- av_price = (s_info.bid + s_info.ask) / 2
498
- trade_risk = self.get_trade_risk()
499
- symbol_type = self.get_symbol_type(self.symbol)
500
- FX = symbol_type == "FX"
501
- COMD = symbol_type == "COMD"
502
- FUT = symbol_type == "FUT"
503
- CRYPTO = symbol_type == "CRYPTO"
504
- if COMD:
505
- supported = _COMMD_SUPPORTED_
506
- if "." in self.symbol:
507
- symbol = self.symbol.split(".")[0]
508
- else:
509
- symbol = self.symbol
510
- if str(symbol) not in supported:
511
- raise ValueError(
512
- f"Currency risk calculation for '{self.symbol}' is not a currently supported. \n"
513
- f"Supported commodity symbols are: {', '.join(supported)}"
514
- )
515
- if FUT:
516
- if "_" in self.symbol:
517
- symbol = self.symbol[:-2]
518
- else:
519
- symbol = self.symbol
520
- supported = _ADMIRAL_MARKETS_FUTURES_ + __PEPPERSTONE_FUTURES__
521
- if str(symbol) not in supported:
522
- raise ValueError(
523
- f"Currency risk calculation for '{self.symbol}' is not a currently supported. \n"
524
- f"Supported future symbols are: {', '.join(supported)}"
525
- )
526
- if trade_risk > 0:
527
- currency_risk = round(self.var_loss_value(), 5)
528
- volume = round(currency_risk * laverage)
529
- try:
530
- _lot = round((volume / (contract_size * av_price)), 2)
531
- except ZeroDivisionError:
532
- _lot = 0.0
533
- lot = self._check_lot(_lot)
534
- if COMD and contract_size > 1:
535
- # lot = volume / av_price / contract_size
536
- try:
537
- lot = volume / av_price / contract_size
538
- except ZeroDivisionError:
539
- lot = 0.0
540
- lot = self._check_lot(_lot)
541
- if FX:
542
- try:
543
- __lot = round((volume / contract_size), 2)
544
- except ZeroDivisionError:
545
- __lot = 0.0
546
- lot = self._check_lot(__lot)
547
-
548
- tick_value = s_info.trade_tick_value
549
- tick_value_loss = s_info.trade_tick_value_loss
550
- tick_value_profit = s_info.trade_tick_value_profit
551
-
552
- if COMD or FUT or CRYPTO and contract_size > 1:
553
- tick_value_loss = tick_value_loss / contract_size
554
- tick_value_profit = tick_value_profit / contract_size
555
- if tick_value == 0 or tick_value_loss == 0 or tick_value_profit == 0:
556
- raise ValueError(
557
- f"""The Tick Values for {self.symbol} is 0.0
558
- We can not procced with currency risk calculation
559
- Please check your Broker trade conditions
560
- and symbol specifications for {self.symbol}"""
561
- )
562
-
563
- # Case where the stop loss is given
564
- if self.sl is not None:
565
- trade_loss = currency_risk / self.sl
566
- if self.tp is not None:
567
- trade_profit = (currency_risk * (self.tp // self.sl)) / self.tp
568
- else:
569
- trade_profit = (currency_risk * self.rr) / (self.sl * self.rr)
570
- lot_ = round(trade_loss / (contract_size * tick_value_loss), 2)
571
- lot = self._check_lot(lot_)
572
- volume = round(lot * contract_size * av_price)
573
-
574
- if COMD or CRYPTO and contract_size > 1:
575
- # trade_risk = points * tick_value_loss * lot
576
- lot = currency_risk / (self.sl * tick_value_loss * contract_size)
577
- lot = self._check_lot(lot)
578
- trade_loss = lot * contract_size * tick_value_loss
579
-
580
- if FX:
581
- volume = round((trade_loss * contract_size) / tick_value_loss)
582
- __lot = round((volume / contract_size), 2)
583
- lot = self._check_lot(__lot)
584
-
585
- # Case where the stantard deviation is used
586
- elif self.std and self.pchange is None and self.sl is None:
587
- sl = self.get_std_stop()
588
- infos = self._std_pchange_stop(
589
- currency_risk, sl, contract_size, tick_value_loss
590
- )
591
- trade_loss, trade_profit, lot, volume = infos
592
-
593
- # Case where the stop loss is based on a percentage change
594
- elif self.pchange is not None and not self.std and self.sl is None:
595
- sl = self.get_pchange_stop(self.pchange)
596
- infos = self._std_pchange_stop(
597
- currency_risk, sl, contract_size, tick_value_loss
598
- )
599
- trade_loss, trade_profit, lot, volume = infos
600
-
601
- # Default cases
602
- else:
603
- # Handle FX
604
- if FX:
605
- trade_loss = tick_value_loss * (volume / contract_size)
606
- trade_profit = tick_value_profit * (volume / contract_size)
607
- else:
608
- trade_loss = (lot * contract_size) * tick_value_loss
609
- trade_profit = (lot * contract_size) * tick_value_profit
610
-
611
- # if self.get_symbol_type(self.symbol) == 'IDX':
612
- # rates = self.get_currency_rates(self.symbol)
613
- # if rates['mc'] == rates['pc'] == 'JPY':
614
- # lot = lot * contract_size
615
- # lot = self._check_lot(lot)
616
- # volume = round(lot * av_price * contract_size)
617
- # if contract_size == 1:
618
- # volume = round(lot * av_price)
619
-
620
- return {
621
- "currency_risk": currency_risk,
622
- "trade_loss": trade_loss,
623
- "trade_profit": trade_profit,
624
- "volume": round(volume),
625
- "lot": lot,
626
- }
627
- else:
628
- return {
629
- "currency_risk": 0.0,
630
- "trade_loss": 0.0,
631
- "trade_profit": 0.0,
632
- "volume": 0,
633
- "lot": 0.01,
634
- }
635
-
636
- def _std_pchange_stop(self, currency_risk, sl, size, loss):
637
- """
638
- Calculate the stop loss level based on standard deviation or percentage change.
639
-
640
- Args:
641
- currency_risk (float): The amount of risk in dollars.
642
- sl (int): Stop loss level in points.
643
- size (int): Contract size.
644
- loss (float): Loss value per tick in dollars.
645
-
646
- """
647
- trade_loss = currency_risk / sl
648
- trade_profit = (currency_risk * self.rr) / (sl * self.rr)
649
- av_price = (self.symbol_info.bid + self.symbol_info.ask) / 2
650
- _lot = round(trade_loss / (size * loss), 2)
651
- lot = self._check_lot(_lot)
652
-
653
- volume = round(lot * size * av_price)
654
- if self.get_symbol_type(self.symbol) == "FX":
655
- volume = round((trade_loss * size) / loss)
656
- __lot = round((volume / size), 2)
657
- lot = self._check_lot(__lot)
658
-
659
- if (
660
- self.get_symbol_type(self.symbol) == "COMD"
661
- or self.get_symbol_type(self.symbol) == "CRYPTO"
662
- and size > 1
663
- ):
664
- lot = currency_risk / (sl * loss * size)
665
- lot = self._check_lot(lot)
666
- trade_loss = lot * size * loss
667
- volume = round(lot * size * av_price)
668
-
669
- return trade_loss, trade_profit, lot, volume
670
-
671
- def _check_lot(self, lot: float) -> float:
672
- if lot > self.symbol_info.volume_max:
673
- return self.symbol_info.volume_max / 2
674
- elif lot < self.symbol_info.volume_min:
675
- return self.symbol_info.volume_min
676
- return lot
677
-
678
- def get_trade_risk(self):
679
- """Calculate risk per trade as percentage"""
680
- total_risk = self.risk_level()
681
- max_trades = self.max_trade()
682
- if total_risk < self.max_risk:
683
- if self.daily_dd is not None:
684
- trade_risk = self.daily_dd / max_trades
685
- else:
686
- trade_risk = (self.max_risk - total_risk) / max_trades
687
- return trade_risk
688
- else:
689
- return 0
690
-
691
- def get_leverage(self, account: bool) -> int:
692
- """
693
- get the Laverage for each symbol
694
-
695
- Args:
696
- account (bool): If set to True, the account leverage will be used
697
- in risk managment setting
698
- Notes:
699
- For FX symbols, account leverage is used by default.
700
- For Other instruments the account leverage is used if any error
701
- occurs in leverage calculation.
702
- """
703
- AL = self.get_account_info().leverage
704
- if account:
705
- return AL
706
-
707
- if self.get_symbol_type(self.symbol) == "FX":
708
- return AL
709
- else:
710
- s_info = self.symbol_info
711
- volume_min = s_info.volume_min
712
- contract_size = s_info.trade_contract_size
713
- av_price = (s_info.bid + s_info.ask) / 2
714
- action = random.choice([Mt5.ORDER_TYPE_BUY, Mt5.ORDER_TYPE_SELL])
715
- margin = Mt5.order_calc_margin(action, self.symbol, volume_min, av_price)
716
- if margin is None:
717
- return AL
718
- try:
719
- leverage = (volume_min * contract_size * av_price) / margin
720
- return round(leverage)
721
- except ZeroDivisionError:
722
- return AL
723
-
724
- def get_deviation(self) -> int:
725
- return self.symbol_info.spread
726
-
727
- def get_break_even(self) -> int:
728
- if self.be is not None:
729
- if isinstance(self.be, int):
730
- return self.be
731
- elif isinstance(self.be, float):
732
- return self.get_pchange_stop(self.be)
733
- else:
734
- stop = self.get_stop_loss()
735
- spread = self.get_symbol_info(self.symbol).spread
736
- if stop <= 100:
737
- be = round((stop + spread) * 0.5)
738
- elif stop > 100 and stop <= 150:
739
- be = round((stop + spread) * 0.35)
740
- elif stop > 150:
741
- be = round((stop + spread) * 0.25)
742
- return be
743
-
744
- def is_risk_ok(self) -> bool:
745
- if self.risk_level() <= self.max_risk:
746
- return True
747
- else:
748
- return False
1
+ import random
2
+ from datetime import datetime
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ from scipy.stats import norm
6
+
7
+ from bbstrader.metatrader.account import Account
8
+ from bbstrader.metatrader.rates import Rates
9
+ from bbstrader.metatrader.utils import TIMEFRAMES, TimeFrame
10
+
11
+ try:
12
+ import MetaTrader5 as Mt5
13
+ except ImportError:
14
+ import bbstrader.compat # noqa: F401
15
+
16
+
17
+ _COMMD_SUPPORTED_ = [
18
+ "GOLD",
19
+ "SILVER",
20
+ "BRENT",
21
+ "CRUDOIL",
22
+ "WTI",
23
+ "UKOIL",
24
+ "XAGEUR",
25
+ "XAGUSD",
26
+ "XAGAUD",
27
+ "XAGGBP",
28
+ "XAUAUD",
29
+ "XAUEUR",
30
+ "XAUUSD",
31
+ "XAUGBP",
32
+ "USOIL",
33
+ "SpotCrude",
34
+ "SpotBrent",
35
+ "Soybeans",
36
+ "Wheat",
37
+ "SoyOil",
38
+ "LeanHogs",
39
+ "LDSugar",
40
+ "Coffee",
41
+ "OJ",
42
+ "Cocoa",
43
+ "Cattle",
44
+ "Copper",
45
+ "XCUUSD",
46
+ "NatGas",
47
+ "Gasoline",
48
+ ]
49
+
50
+ _ADMIRAL_MARKETS_FUTURES_ = [
51
+ "#USTNote_",
52
+ "#Bund_",
53
+ "#USDX_",
54
+ "_AUS200_",
55
+ "_Canada60_",
56
+ "_SouthAfrica40_",
57
+ "_STXE600_",
58
+ "_EURO50_",
59
+ "_GER40_",
60
+ "_GermanyTech30_",
61
+ "_MidCapGER50_",
62
+ "_SWISS20_",
63
+ "_UK100_",
64
+ "_USNASDAQ100_",
65
+ "_YM_",
66
+ "_ES_",
67
+ "_CrudeOilUS_",
68
+ "_DUTCH25_",
69
+ "_FRANCE40_",
70
+ "_NORWAY25_",
71
+ "_SPAIN35_",
72
+ "_CrudeOilUK_",
73
+ "_XAU_",
74
+ "_HK50_",
75
+ "_HSCEI50_",
76
+ ]
77
+
78
+ __PEPPERSTONE_FUTURES__ = [
79
+ "AUS200-F",
80
+ "GER40-F",
81
+ "HK50-F",
82
+ "JPN225-F",
83
+ "UK100-F",
84
+ "US30-F",
85
+ "NAS100-F",
86
+ "US500-F",
87
+ "Crude-F",
88
+ "Brent-F",
89
+ "XAUUSD-F",
90
+ "XAGUSD-F",
91
+ "USDX-F",
92
+ "EUSTX50-F",
93
+ "FRA40-F",
94
+ "GERTEC30-F",
95
+ "SPA35-F",
96
+ "SWI20-F",
97
+ ]
98
+
99
+ __all__ = ["RiskManagement"]
100
+
101
+
102
+ class RiskManagement(Account):
103
+ """
104
+ The RiskManagement class provides foundational
105
+ risk management functionalities for trading activities.
106
+ It calculates risk levels, determines stop loss and take profit levels,
107
+ and ensures trading activities align with predefined risk parameters.
108
+
109
+ Exemple:
110
+ >>> risk_manager = RiskManagement(
111
+ ... symbol="EURUSD",
112
+ ... max_risk=5.0,
113
+ ... daily_risk=2.0,
114
+ ... max_trades=10,
115
+ ... std_stop=True,
116
+ ... account_leverage=True,
117
+ ... start_time="09:00",
118
+ ... finishing_time="17:00",
119
+ ... time_frame="1h"
120
+ ... )
121
+ >>> # Calculate risk level
122
+ >>> risk_level = risk_manager.risk_level()
123
+
124
+ >>> # Get appropriate lot size for a trade
125
+ >>> lot_size = risk_manager.get_lot()
126
+
127
+ >>> # Determine stop loss and take profit levels
128
+ >>> stop_loss = risk_manager.get_stop_loss()
129
+ >>> take_profit = risk_manager.get_take_profit()
130
+
131
+ >>> # Check if current risk is acceptable
132
+ >>> is_risk_acceptable = risk_manager.is_risk_ok()
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ symbol: str,
138
+ max_risk: float = 10.0,
139
+ daily_risk: Optional[float] = None,
140
+ max_trades: Optional[int] = None,
141
+ std_stop: bool = False,
142
+ pchange_sl: Optional[float] = None,
143
+ var_level: float = 0.95,
144
+ var_time_frame: TimeFrame = "D1",
145
+ account_leverage: bool = True,
146
+ time_frame: TimeFrame = "D1",
147
+ start_time: str = "1:00",
148
+ finishing_time: str = "23:00",
149
+ sl: Optional[int] = None,
150
+ tp: Optional[int] = None,
151
+ be: Optional[int] = None,
152
+ rr: float = 1.5,
153
+ **kwargs,
154
+ ):
155
+ """
156
+ Initialize the RiskManagement class to manage risk in trading activities.
157
+
158
+ Args:
159
+ symbol (str): The symbol of the financial instrument to trade.
160
+ max_risk (float): The `maximum risk allowed` on the trading account.
161
+ daily_risk (float, optional): `Daily Max risk allowed`.
162
+ If Set to None it will be determine based on Maximum risk.
163
+ max_trades (int, optional): Maximum number of trades in a trading session.
164
+ If set to None it will be determine based on the timeframe of trading.
165
+ std_stop (bool, optional): If set to True, the Stop loss is calculated based
166
+ On `historical volatility` of the trading instrument. Defaults to False.
167
+ pchange_sl (float, optional): If set, the Stop loss is calculated based
168
+ On `percentage change` of the trading instrument.
169
+ var_level (float, optional): Confidence level for Value-at-Risk,e.g., 0.99 for 99% confidence interval.
170
+ The default is 0.95.
171
+ var_time_frame (str, optional): Time frame to use to calculate the VaR.
172
+ account_leverage (bool, optional): If set to True the account leverage will be used
173
+ In risk management setting. Defaults to False.
174
+ time_frame (str, optional): The time frame on which the program is working
175
+ `(1m, 3m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, D1)`. Defaults to 'D1'.
176
+ start_time (str, optional): The starting time for the trading strategy
177
+ `(HH:MM, H an M do not star with 0)`. Defaults to "6:30".
178
+ finishing_time (str, optional): The finishing time for the trading strategy
179
+ `(HH:MM, H an M do not star with 0)`. Defaults to "19:30".
180
+ sl (int, optional): Stop Loss in points, Must be a positive number.
181
+ tp (int, optional): Take Profit in points, Must be a positive number.
182
+ be (int, optional): Break Even in points, Must be a positive number.
183
+ rr (float, optional): Risk reward ratio, Must be a positive number. Defaults to 1.5.
184
+
185
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
186
+ """
187
+ super().__init__(**kwargs)
188
+
189
+ # Validation
190
+ if daily_risk is not None and daily_risk < 0:
191
+ raise ValueError("daily_risk must be a positive number")
192
+ if max_risk <= 0:
193
+ raise ValueError("max_risk must be a positive number")
194
+ if sl is not None and not isinstance(sl, int):
195
+ raise ValueError("sl must be an integer number")
196
+ if tp is not None and not isinstance(tp, int):
197
+ raise ValueError("tp must be an integer number")
198
+ if be is not None and (not isinstance(be, int) or be <= 0):
199
+ raise ValueError("be must be a positive integer number")
200
+ if time_frame not in TIMEFRAMES:
201
+ raise ValueError("Unsupported time frame {}".format(time_frame))
202
+ if var_time_frame not in TIMEFRAMES:
203
+ raise ValueError("Unsupported time frame {}".format(var_time_frame))
204
+
205
+ self.kwargs = kwargs
206
+ self.symbol = symbol
207
+ self.start_time = start_time
208
+ self.finishing_time = finishing_time
209
+ self.max_trades = max_trades
210
+ self.std = std_stop
211
+ self.pchange = pchange_sl
212
+ self.var_level = var_level
213
+ self.var_tf = var_time_frame
214
+ self.daily_dd = round(daily_risk, 5) if daily_risk is not None else None
215
+ self.max_risk = max_risk
216
+ self.rr = rr
217
+ self.sl = sl
218
+ self.tp = tp
219
+ self.be = be
220
+
221
+ self.account_leverage = account_leverage
222
+ self.symbol_info = super().get_symbol_info(self.symbol)
223
+ self.copy_mode = kwargs.get("copy", False)
224
+
225
+ self._tf = time_frame
226
+
227
+ @property
228
+ def dailydd(self) -> float:
229
+ return self.daily_dd
230
+
231
+ @dailydd.setter
232
+ def dailydd(self, value: float):
233
+ self.daily_dd = value
234
+
235
+ @property
236
+ def maxrisk(self) -> float:
237
+ return self.max_risk
238
+
239
+ @maxrisk.setter
240
+ def maxrisk(self, value: float):
241
+ self.max_risk = value
242
+
243
+ def _convert_time_frame(self, tf: str) -> int:
244
+ """Convert time frame to minutes"""
245
+ if tf == "D1":
246
+ tf_int = self.get_minutes()
247
+ elif "m" in tf:
248
+ tf_int = TIMEFRAMES[tf]
249
+ elif "h" in tf:
250
+ tf_int = int(tf[0]) * 60
251
+ elif tf == "W1":
252
+ tf_int = self.get_minutes() * 5
253
+ elif tf == "MN1":
254
+ tf_int = self.get_minutes() * 22
255
+ return tf_int
256
+
257
+ def risk_level(self) -> float:
258
+ """
259
+ Calculates the risk level of a trade
260
+
261
+ Returns:
262
+ - Risk level in the form of a float percentage.
263
+ """
264
+ account_info = self.get_account_info()
265
+ balance = account_info.balance
266
+ equity = account_info.equity
267
+ df = self.get_trades_history()
268
+ if df is None:
269
+ profit = 0
270
+ else:
271
+ profit_df = df.iloc[1:]
272
+ profit = profit_df["profit"].sum()
273
+ commisions = df["commission"].sum()
274
+ fees = df["fee"].sum()
275
+ swap = df["swap"].sum()
276
+ total_profit = commisions + fees + swap + profit
277
+ initial_balance = balance - total_profit
278
+ if balance != 0:
279
+ risk_alowed = (((equity - initial_balance) / equity) * 100) * -1
280
+ return round(risk_alowed, 2)
281
+ return 0.0
282
+
283
+ def get_lot(self) -> float:
284
+ """ "Get the approprite lot size for a trade"""
285
+ s_info = self.symbol_info
286
+ volume_step = s_info.volume_step
287
+ lot = self.currency_risk()["lot"]
288
+ steps = self._volume_step(volume_step)
289
+ if float(steps) >= float(1):
290
+ return round(lot, steps)
291
+ else:
292
+ return round(lot)
293
+
294
+ def _volume_step(self, value):
295
+ """Get the number of decimal places in a number"""
296
+
297
+ value_str = str(value)
298
+
299
+ if "." in value_str and value_str != "1.0":
300
+ decimal_index = value_str.index(".")
301
+ num_digits = len(value_str) - decimal_index - 1
302
+ return num_digits
303
+
304
+ elif value_str == "1.0":
305
+ return 0
306
+ else:
307
+ return 0
308
+
309
+ def max_trade(self) -> int:
310
+ """calculates the maximum number of trades allowed"""
311
+ minutes = self.get_minutes()
312
+ tf_int = self._convert_time_frame(self._tf)
313
+ if self.max_trades is not None:
314
+ max_trades = self.max_trades
315
+ else:
316
+ max_trades = round(minutes / tf_int)
317
+ return max(max_trades, 1)
318
+
319
+ def get_minutes(self) -> int:
320
+ """calculates the number of minutes between two times"""
321
+
322
+ start = datetime.strptime(self.start_time, "%H:%M")
323
+ end = datetime.strptime(self.finishing_time, "%H:%M")
324
+ return (end - start).total_seconds() // 60
325
+
326
+ def get_hours(self) -> int:
327
+ """Calculates the number of hours between two times"""
328
+
329
+ start = datetime.strptime(self.start_time, "%H:%M")
330
+ end = datetime.strptime(self.finishing_time, "%H:%M")
331
+ # Calculate the difference in hours
332
+ hours = (end - start).total_seconds() // 3600 # 1 hour = 3600 seconds
333
+
334
+ return hours
335
+
336
+ def get_std_stop(self):
337
+ """
338
+ Calculate the standard deviation-based stop loss level
339
+ for a given financial instrument.
340
+
341
+ Returns:
342
+ - Standard deviation-based stop loss level, rounded to the nearest point.
343
+ - 0 if the calculated stop loss is less than or equal to 0.
344
+ """
345
+ minutes = self.get_minutes()
346
+ tf_int = self._convert_time_frame(self._tf)
347
+ interval = round((minutes / tf_int) * 252)
348
+
349
+ rate = Rates(
350
+ self.symbol, timeframe=self._tf, start_pos=0, count=interval, **self.kwargs
351
+ )
352
+ returns = rate.returns * 100
353
+ std = returns.std()
354
+ point = self.get_symbol_info(self.symbol).point
355
+ av_price = (self.symbol_info.bid + self.symbol_info.ask) / 2
356
+ price_interval = av_price * (100 - std) / 100
357
+ sl_point = float((av_price - price_interval) / point)
358
+ sl = round(sl_point)
359
+ min_sl = self.symbol_info.trade_stops_level * 2 + self.get_deviation()
360
+
361
+ return max(sl, min_sl)
362
+
363
+ def get_pchange_stop(self, pchange: Optional[float]):
364
+ """
365
+ Calculate the percentage change-based stop loss level
366
+ for a given financial instrument.
367
+
368
+ Args:
369
+ pchange (float): Percentage change in price to use for calculating stop loss level.
370
+ If pchange is set to None, the stop loss is calculate using std.
371
+
372
+ Returns:
373
+ - Percentage change-based stop loss level, rounded to the nearest point.
374
+ - 0 if the calculated stop loss is <= 0.
375
+ """
376
+ if pchange is not None:
377
+ av_price = (self.symbol_info.bid + self.symbol_info.ask) / 2
378
+ price_interval = av_price * (100 - pchange) / 100
379
+ point = self.get_symbol_info(self.symbol).point
380
+ sl_point = float((av_price - price_interval) / point)
381
+ sl = round(sl_point)
382
+ min_sl = self.symbol_info.trade_stops_level * 2 + self.get_deviation()
383
+ return max(sl, min_sl)
384
+ else:
385
+ # Use std as default pchange
386
+ return self.get_std_stop()
387
+
388
+ def calculate_var(self, tf: TimeFrame = "D1", c=0.95):
389
+ """
390
+ Calculate Value at Risk (VaR) for a given portfolio.
391
+
392
+ Args:
393
+ tf (str): Time frame to use to calculate volatility.
394
+ c (float): Confidence level for VaR calculation (default is 95%).
395
+
396
+ Returns:
397
+ - VaR value
398
+ """
399
+ minutes = self.get_minutes()
400
+ tf_int = self._convert_time_frame(tf)
401
+ interval = round((minutes / tf_int) * 252)
402
+
403
+ rate = Rates(
404
+ self.symbol, timeframe=tf, start_pos=0, count=interval, **self.kwargs
405
+ )
406
+ returns = rate.returns * 100
407
+ p = self.get_account_info().margin_free
408
+ mu = returns.mean()
409
+ sigma = returns.std()
410
+ var = self.var_cov_var(p, c, mu, sigma)
411
+ return var
412
+
413
+ def var_cov_var(self, P: float, c: float, mu: float, sigma: float):
414
+ """
415
+ Variance-Covariance calculation of daily Value-at-Risk.
416
+
417
+ Args:
418
+ P (float): Portfolio value in USD.
419
+ c (float): Confidence level for Value-at-Risk,e.g., 0.99 for 99% confidence interval.
420
+ mu (float): Mean of the returns of the portfolio.
421
+ sigma (float): Standard deviation of the returns of the portfolio.
422
+
423
+ Returns:
424
+ - float: Value-at-Risk for the given portfolio.
425
+ """
426
+ alpha = norm.ppf(1 - c, mu, sigma)
427
+ return P - P * (alpha + 1)
428
+
429
+ def var_loss_value(self):
430
+ """
431
+ Calculate the stop-loss level based on VaR.
432
+
433
+ Notes:
434
+ The Var is Estimated using the Variance-Covariance method on the daily returns.
435
+ If you want to use the VaR for a different time frame .
436
+ """
437
+ P = self.get_account_info().margin_free
438
+ trade_risk = self.get_trade_risk()
439
+ loss_allowed = P * trade_risk / 100
440
+ var = self.calculate_var(c=self.var_level, tf=self.var_tf)
441
+ return min(var, loss_allowed)
442
+
443
+ def get_take_profit(self) -> int:
444
+ """calculates the take profit of a trade in points"""
445
+ deviation = self.get_deviation()
446
+ if self.tp is not None:
447
+ return self.tp + deviation
448
+ else:
449
+ return round(self.get_stop_loss() * self.rr)
450
+
451
+ def get_stop_loss(self) -> int:
452
+ """calculates the stop loss of a trade in points"""
453
+ min_sl = self.symbol_info.trade_stops_level * 2 + self.get_deviation()
454
+ if self.sl is not None:
455
+ return max(self.sl, min_sl)
456
+ elif self.sl is None and self.std:
457
+ sl = self.get_std_stop()
458
+ return max(sl, min_sl)
459
+ elif self.sl is None and not self.std:
460
+ risk = self.currency_risk()
461
+ if risk["trade_loss"] != 0:
462
+ sl = round((risk["currency_risk"] / risk["trade_loss"]))
463
+ return max(sl, min_sl)
464
+ return min_sl
465
+
466
+ def get_currency_risk(self) -> float:
467
+ """calculates the currency risk of a trade"""
468
+ return round(self.currency_risk()["currency_risk"], 2)
469
+
470
+ def expected_profit(self):
471
+ """Calculate the expected profit per trade"""
472
+ risk = self.get_currency_risk()
473
+ return round(risk * self.rr, 2)
474
+
475
+ def volume(self):
476
+ """Volume per trade"""
477
+
478
+ return self.currency_risk()["volume"]
479
+
480
+ def currency_risk(self) -> Dict[str, Union[int, float, Any]]:
481
+ """
482
+ Calculates the currency risk of a trade.
483
+
484
+ Returns:
485
+ Dict[str, Union[int, float, Any]]: A dictionary containing the following keys:
486
+
487
+ - `'currency_risk'`: Dollar amount risk on a single trade.
488
+ - `'trade_loss'`: Loss value per tick in dollars.
489
+ - `'trade_profit'`: Profit value per tick in dollars.
490
+ - `'volume'`: Contract size multiplied by the average price.
491
+ - `'lot'`: Lot size per trade.
492
+ """
493
+ s_info = self.symbol_info
494
+
495
+ laverage = self.get_leverage(self.account_leverage)
496
+ contract_size = s_info.trade_contract_size
497
+
498
+ av_price = (s_info.bid + s_info.ask) / 2
499
+ trade_risk = self.get_trade_risk()
500
+ symbol_type = self.get_symbol_type(self.symbol)
501
+ FX = symbol_type == "FX"
502
+ COMD = symbol_type == "COMD"
503
+ FUT = symbol_type == "FUT"
504
+ CRYPTO = symbol_type == "CRYPTO"
505
+ if COMD:
506
+ supported = _COMMD_SUPPORTED_
507
+ if "." in self.symbol:
508
+ symbol = self.symbol.split(".")[0]
509
+ else:
510
+ symbol = self.symbol
511
+ if not self.copy_mode and str(symbol) not in supported:
512
+ raise ValueError(
513
+ f"Currency risk calculation for '{self.symbol}' is not a currently supported. \n"
514
+ f"Supported commodity symbols are: {', '.join(supported)}"
515
+ )
516
+ if FUT:
517
+ if "_" in self.symbol:
518
+ symbol = self.symbol[:-2]
519
+ else:
520
+ symbol = self.symbol
521
+ supported = _ADMIRAL_MARKETS_FUTURES_ + __PEPPERSTONE_FUTURES__
522
+ if not self.copy_mode and str(symbol) not in supported:
523
+ raise ValueError(
524
+ f"Currency risk calculation for '{self.symbol}' is not a currently supported. \n"
525
+ f"Supported future symbols are: {', '.join(supported)}"
526
+ )
527
+ if trade_risk > 0:
528
+ currency_risk = round(self.var_loss_value(), 5)
529
+ volume = round(currency_risk * laverage)
530
+ try:
531
+ _lot = round((volume / (contract_size * av_price)), 2)
532
+ except ZeroDivisionError:
533
+ _lot = 0.0
534
+ lot = self._check_lot(_lot)
535
+ if COMD and contract_size > 1:
536
+ # lot = volume / av_price / contract_size
537
+ try:
538
+ lot = volume / av_price / contract_size
539
+ except ZeroDivisionError:
540
+ lot = 0.0
541
+ lot = self._check_lot(_lot)
542
+ if FX:
543
+ try:
544
+ __lot = round((volume / contract_size), 2)
545
+ except ZeroDivisionError:
546
+ __lot = 0.0
547
+ lot = self._check_lot(__lot)
548
+
549
+ tick_value = s_info.trade_tick_value
550
+ tick_value_loss = s_info.trade_tick_value_loss
551
+ tick_value_profit = s_info.trade_tick_value_profit
552
+
553
+ if COMD or FUT or CRYPTO and contract_size > 1:
554
+ tick_value_loss = tick_value_loss / contract_size
555
+ tick_value_profit = tick_value_profit / contract_size
556
+ if tick_value == 0 or tick_value_loss == 0 or tick_value_profit == 0:
557
+ raise ValueError(
558
+ f"""The Tick Values for {self.symbol} is 0.0
559
+ We can not procced with currency risk calculation
560
+ Please check your Broker trade conditions
561
+ and symbol specifications for {self.symbol}"""
562
+ )
563
+
564
+ # Case where the stop loss is given
565
+ if self.sl is not None:
566
+ trade_loss = currency_risk / self.sl
567
+ if self.tp is not None:
568
+ trade_profit = (currency_risk * (self.tp // self.sl)) / self.tp
569
+ else:
570
+ trade_profit = (currency_risk * self.rr) / (self.sl * self.rr)
571
+ lot_ = round(trade_loss / (contract_size * tick_value_loss), 2)
572
+ lot = self._check_lot(lot_)
573
+ volume = round(lot * contract_size * av_price)
574
+
575
+ if COMD or CRYPTO and contract_size > 1:
576
+ # trade_risk = points * tick_value_loss * lot
577
+ lot = currency_risk / (self.sl * tick_value_loss * contract_size)
578
+ lot = self._check_lot(lot)
579
+ trade_loss = lot * contract_size * tick_value_loss
580
+
581
+ if FX:
582
+ volume = round((trade_loss * contract_size) / tick_value_loss)
583
+ __lot = round((volume / contract_size), 2)
584
+ lot = self._check_lot(__lot)
585
+
586
+ # Case where the stantard deviation is used
587
+ elif self.std and self.pchange is None and self.sl is None:
588
+ sl = self.get_std_stop()
589
+ infos = self._std_pchange_stop(
590
+ currency_risk, sl, contract_size, tick_value_loss
591
+ )
592
+ trade_loss, trade_profit, lot, volume = infos
593
+
594
+ # Case where the stop loss is based on a percentage change
595
+ elif self.pchange is not None and not self.std and self.sl is None:
596
+ sl = self.get_pchange_stop(self.pchange)
597
+ infos = self._std_pchange_stop(
598
+ currency_risk, sl, contract_size, tick_value_loss
599
+ )
600
+ trade_loss, trade_profit, lot, volume = infos
601
+
602
+ # Default cases
603
+ else:
604
+ # Handle FX
605
+ if FX:
606
+ trade_loss = tick_value_loss * (volume / contract_size)
607
+ trade_profit = tick_value_profit * (volume / contract_size)
608
+ else:
609
+ trade_loss = (lot * contract_size) * tick_value_loss
610
+ trade_profit = (lot * contract_size) * tick_value_profit
611
+
612
+ # if self.get_symbol_type(self.symbol) == 'IDX':
613
+ # rates = self.get_currency_rates(self.symbol)
614
+ # if rates['mc'] == rates['pc'] == 'JPY':
615
+ # lot = lot * contract_size
616
+ # lot = self._check_lot(lot)
617
+ # volume = round(lot * av_price * contract_size)
618
+ # if contract_size == 1:
619
+ # volume = round(lot * av_price)
620
+
621
+ return {
622
+ "currency_risk": currency_risk,
623
+ "trade_loss": trade_loss,
624
+ "trade_profit": trade_profit,
625
+ "volume": round(volume),
626
+ "lot": lot,
627
+ }
628
+ else:
629
+ return {
630
+ "currency_risk": 0.0,
631
+ "trade_loss": 0.0,
632
+ "trade_profit": 0.0,
633
+ "volume": 0,
634
+ "lot": 0.01,
635
+ }
636
+
637
+ def _std_pchange_stop(self, currency_risk, sl, size, loss):
638
+ """
639
+ Calculate the stop loss level based on standard deviation or percentage change.
640
+
641
+ Args:
642
+ currency_risk (float): The amount of risk in dollars.
643
+ sl (int): Stop loss level in points.
644
+ size (int): Contract size.
645
+ loss (float): Loss value per tick in dollars.
646
+
647
+ """
648
+ trade_loss = currency_risk / sl
649
+ trade_profit = (currency_risk * self.rr) / (sl * self.rr)
650
+ av_price = (self.symbol_info.bid + self.symbol_info.ask) / 2
651
+ _lot = round(trade_loss / (size * loss), 2)
652
+ lot = self._check_lot(_lot)
653
+
654
+ volume = round(lot * size * av_price)
655
+ if self.get_symbol_type(self.symbol) == "FX":
656
+ volume = round((trade_loss * size) / loss)
657
+ __lot = round((volume / size), 2)
658
+ lot = self._check_lot(__lot)
659
+
660
+ if (
661
+ self.get_symbol_type(self.symbol) == "COMD"
662
+ or self.get_symbol_type(self.symbol) == "CRYPTO"
663
+ and size > 1
664
+ ):
665
+ lot = currency_risk / (sl * loss * size)
666
+ lot = self._check_lot(lot)
667
+ trade_loss = lot * size * loss
668
+ volume = round(lot * size * av_price)
669
+
670
+ return trade_loss, trade_profit, lot, volume
671
+
672
+ def _check_lot(self, lot: float) -> float:
673
+ if lot > self.symbol_info.volume_max:
674
+ return self.symbol_info.volume_max / 2
675
+ elif lot < self.symbol_info.volume_min:
676
+ return self.symbol_info.volume_min
677
+ return lot
678
+
679
+ def get_trade_risk(self):
680
+ """Calculate risk per trade as percentage"""
681
+ total_risk = self.risk_level()
682
+ max_trades = self.max_trade()
683
+ if total_risk < self.max_risk:
684
+ if self.daily_dd is not None:
685
+ trade_risk = self.daily_dd / max_trades
686
+ else:
687
+ trade_risk = (self.max_risk - total_risk) / max_trades
688
+ return trade_risk
689
+ else:
690
+ return 0
691
+
692
+ def get_leverage(self, account: bool) -> int:
693
+ """
694
+ get the Laverage for each symbol
695
+
696
+ Args:
697
+ account (bool): If set to True, the account leverage will be used
698
+ in risk managment setting
699
+ Notes:
700
+ For FX symbols, account leverage is used by default.
701
+ For Other instruments the account leverage is used if any error
702
+ occurs in leverage calculation.
703
+ """
704
+ AL = self.get_account_info().leverage
705
+ if account:
706
+ return AL
707
+
708
+ if self.get_symbol_type(self.symbol) == "FX":
709
+ return AL
710
+ else:
711
+ s_info = self.symbol_info
712
+ volume_min = s_info.volume_min
713
+ contract_size = s_info.trade_contract_size
714
+ av_price = (s_info.bid + s_info.ask) / 2
715
+ action = random.choice([Mt5.ORDER_TYPE_BUY, Mt5.ORDER_TYPE_SELL])
716
+ margin = Mt5.order_calc_margin(action, self.symbol, volume_min, av_price)
717
+ if margin is None:
718
+ return AL
719
+ try:
720
+ leverage = (volume_min * contract_size * av_price) / margin
721
+ return round(leverage)
722
+ except ZeroDivisionError:
723
+ return AL
724
+
725
+ def get_deviation(self) -> int:
726
+ return self.symbol_info.spread
727
+
728
+ def get_break_even(self) -> int:
729
+ if self.be is not None:
730
+ if isinstance(self.be, int):
731
+ return self.be
732
+ elif isinstance(self.be, float):
733
+ return self.get_pchange_stop(self.be)
734
+ else:
735
+ stop = self.get_stop_loss()
736
+ spread = self.get_symbol_info(self.symbol).spread
737
+ if stop <= 100:
738
+ be = round((stop + spread) * 0.5)
739
+ elif stop > 100 and stop <= 150:
740
+ be = round((stop + spread) * 0.35)
741
+ elif stop > 150:
742
+ be = round((stop + spread) * 0.25)
743
+ return be
744
+
745
+ def is_risk_ok(self) -> bool:
746
+ if self.risk_level() <= self.max_risk:
747
+ return True
748
+ else:
749
+ return False