bbstrader 0.2.93__py3-none-any.whl → 0.2.94__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bbstrader might be problematic. Click here for more details.
- bbstrader/__ini__.py +20 -20
- bbstrader/__main__.py +50 -50
- bbstrader/btengine/__init__.py +54 -54
- bbstrader/btengine/scripts.py +157 -157
- bbstrader/compat.py +19 -19
- bbstrader/config.py +137 -137
- bbstrader/core/data.py +22 -22
- bbstrader/core/utils.py +146 -146
- bbstrader/metatrader/__init__.py +6 -6
- bbstrader/metatrader/account.py +1516 -1516
- bbstrader/metatrader/copier.py +750 -745
- bbstrader/metatrader/rates.py +584 -584
- bbstrader/metatrader/risk.py +749 -748
- bbstrader/metatrader/scripts.py +81 -81
- bbstrader/metatrader/trade.py +1836 -1836
- bbstrader/metatrader/utils.py +645 -645
- bbstrader/models/__init__.py +10 -10
- bbstrader/models/factors.py +312 -312
- bbstrader/models/ml.py +1272 -1272
- bbstrader/models/optimization.py +182 -182
- bbstrader/models/portfolio.py +223 -223
- bbstrader/models/risk.py +398 -398
- bbstrader/trading/__init__.py +11 -11
- bbstrader/trading/execution.py +846 -846
- bbstrader/trading/script.py +155 -155
- bbstrader/trading/scripts.py +69 -69
- bbstrader/trading/strategies.py +860 -860
- bbstrader/tseries.py +1842 -1842
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/LICENSE +21 -21
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/METADATA +188 -187
- bbstrader-0.2.94.dist-info/RECORD +44 -0
- bbstrader-0.2.93.dist-info/RECORD +0 -44
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/risk.py
CHANGED
|
@@ -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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
- `'
|
|
488
|
-
- `'
|
|
489
|
-
- `'
|
|
490
|
-
- `'
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
f"
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
f"
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
lot = self.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
#
|
|
613
|
-
#
|
|
614
|
-
#
|
|
615
|
-
# lot =
|
|
616
|
-
#
|
|
617
|
-
#
|
|
618
|
-
#
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
"
|
|
623
|
-
"
|
|
624
|
-
"
|
|
625
|
-
"
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
"
|
|
631
|
-
"
|
|
632
|
-
"
|
|
633
|
-
"
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
lot =
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
For
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|