bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.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.
- bbstrader/__init__.py +27 -0
- bbstrader/__main__.py +92 -0
- bbstrader/api/__init__.py +96 -0
- bbstrader/api/handlers.py +245 -0
- bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
- bbstrader/api/metatrader_client.pyi +624 -0
- bbstrader/assets/bbs_.png +0 -0
- bbstrader/assets/bbstrader.ico +0 -0
- bbstrader/assets/bbstrader.png +0 -0
- bbstrader/assets/qs_metrics_1.png +0 -0
- bbstrader/btengine/__init__.py +54 -0
- bbstrader/btengine/backtest.py +358 -0
- bbstrader/btengine/data.py +737 -0
- bbstrader/btengine/event.py +229 -0
- bbstrader/btengine/execution.py +287 -0
- bbstrader/btengine/performance.py +408 -0
- bbstrader/btengine/portfolio.py +393 -0
- bbstrader/btengine/strategy.py +588 -0
- bbstrader/compat.py +28 -0
- bbstrader/config.py +100 -0
- bbstrader/core/__init__.py +27 -0
- bbstrader/core/data.py +628 -0
- bbstrader/core/strategy.py +466 -0
- bbstrader/metatrader/__init__.py +48 -0
- bbstrader/metatrader/_copier.py +720 -0
- bbstrader/metatrader/account.py +865 -0
- bbstrader/metatrader/broker.py +418 -0
- bbstrader/metatrader/copier.py +1487 -0
- bbstrader/metatrader/rates.py +495 -0
- bbstrader/metatrader/risk.py +667 -0
- bbstrader/metatrader/trade.py +1692 -0
- bbstrader/metatrader/utils.py +402 -0
- bbstrader/models/__init__.py +39 -0
- bbstrader/models/nlp.py +932 -0
- bbstrader/models/optimization.py +182 -0
- bbstrader/scripts.py +665 -0
- bbstrader/trading/__init__.py +33 -0
- bbstrader/trading/execution.py +1159 -0
- bbstrader/trading/strategy.py +362 -0
- bbstrader/trading/utils.py +69 -0
- bbstrader-2.0.3.dist-info/METADATA +396 -0
- bbstrader-2.0.3.dist-info/RECORD +45 -0
- bbstrader-2.0.3.dist-info/WHEEL +5 -0
- bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
- bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from scipy.stats import norm
|
|
7
|
+
|
|
8
|
+
from bbstrader.api import Mt5client as client
|
|
9
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
10
|
+
from bbstrader.metatrader.account import Account
|
|
11
|
+
from bbstrader.metatrader.utils import TIMEFRAMES, SymbolType, TimeFrame
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import MetaTrader5 as mt5
|
|
15
|
+
except ImportError:
|
|
16
|
+
import bbstrader.compat # noqa: F401
|
|
17
|
+
|
|
18
|
+
logger.add(
|
|
19
|
+
f"{BBSTRADER_DIR}/logs/trade.log",
|
|
20
|
+
enqueue=True,
|
|
21
|
+
level="INFO",
|
|
22
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["RiskManagement"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RiskManagement:
|
|
30
|
+
"""
|
|
31
|
+
The RiskManagement class provides foundational
|
|
32
|
+
risk management functionalities for trading activities.
|
|
33
|
+
It calculates risk levels, determines stop loss and take profit levels,
|
|
34
|
+
and ensures trading activities align with predefined risk parameters.
|
|
35
|
+
|
|
36
|
+
Exemple:
|
|
37
|
+
>>> risk_manager = RiskManagement(
|
|
38
|
+
... symbol="EURUSD",
|
|
39
|
+
... max_risk=5.0,
|
|
40
|
+
... daily_risk=2.0,
|
|
41
|
+
... max_trades=10,
|
|
42
|
+
... std_stop=True,
|
|
43
|
+
... act_leverage=True,
|
|
44
|
+
... start_time="09:00",
|
|
45
|
+
... finishing_time="17:00",
|
|
46
|
+
... time_frame="1h"
|
|
47
|
+
... )
|
|
48
|
+
>>> # Calculate risk level
|
|
49
|
+
>>> risk_level = risk_manager.risk_level()
|
|
50
|
+
|
|
51
|
+
>>> # Get appropriate lot size for a trade
|
|
52
|
+
>>> lot_size = risk_manager.get_lot()
|
|
53
|
+
|
|
54
|
+
>>> # Determine stop loss and take profit levels
|
|
55
|
+
>>> stop_loss = risk_manager.get_stop_loss()
|
|
56
|
+
>>> take_profit = risk_manager.get_take_profit()
|
|
57
|
+
|
|
58
|
+
>>> # Check if current risk is acceptable
|
|
59
|
+
>>> is_risk_acceptable = risk_manager.is_risk_ok()
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
symbol: str,
|
|
65
|
+
max_risk: float = 10.0,
|
|
66
|
+
daily_risk: Optional[float] = None,
|
|
67
|
+
max_trades: Optional[int] = None,
|
|
68
|
+
std_stop: bool = False,
|
|
69
|
+
pchange_sl: Optional[float] = None,
|
|
70
|
+
accountt_leverage: bool = True,
|
|
71
|
+
time_frame: TimeFrame = "D1",
|
|
72
|
+
start_time: str = "1:00",
|
|
73
|
+
finishing_time: str = "23:00",
|
|
74
|
+
broker_tz: bool = False,
|
|
75
|
+
sl: Optional[int] = None,
|
|
76
|
+
tp: Optional[int] = None,
|
|
77
|
+
be: Optional[int] = None,
|
|
78
|
+
rr: float = 3.0,
|
|
79
|
+
**kwargs,
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Initialize the RiskManagement class to manage risk in trading activities.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
symbol (str): The symbol of the financial instrument to trade.
|
|
86
|
+
max_risk (float): The `maximum risk allowed` on the trading account.
|
|
87
|
+
daily_risk (float, optional): `Daily Max risk allowed`.
|
|
88
|
+
If Set to None it will be determine based on Maximum risk.
|
|
89
|
+
The day is based on the start and the ending time
|
|
90
|
+
max_trades (int, optional): Maximum number of trades at any point in time.
|
|
91
|
+
If set to None it will be determine based on the timeframe of trading.
|
|
92
|
+
std_stop (bool, optional): If set to True, the Stop loss is calculated based
|
|
93
|
+
On `historical volatility` of the trading instrument. Defaults to False.
|
|
94
|
+
pchange_sl (float, optional): If set, the Stop loss is calculated based
|
|
95
|
+
On `percentage change` of the trading instrument.
|
|
96
|
+
act_leverage (bool, optional): If set to True the account leverage will be used
|
|
97
|
+
In risk management setting. Defaults to False.
|
|
98
|
+
time_frame (str, optional): The time frame on which the program is working
|
|
99
|
+
`(1m, 3m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, D1)`. Defaults to 'D1'.
|
|
100
|
+
start_time (str, optional): The starting time for the trading session
|
|
101
|
+
`(HH:MM, H and M do not star with 0)`. Defaults to "1:00".
|
|
102
|
+
finishing_time (str, optional): The finishing time for the trading strategy
|
|
103
|
+
`(HH:MM, H and M do not star with 0)`. Defaults to "23:00".
|
|
104
|
+
sl (int, optional): Stop Loss in points, Must be a positive number.
|
|
105
|
+
tp (int, optional): Take Profit in points, Must be a positive number.
|
|
106
|
+
be (int, optional): Break Even in points, Must be a positive number.
|
|
107
|
+
rr (float, optional): Risk reward ratio, Must be a positive number. Defaults to 1.5.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
assert max_risk > 0
|
|
111
|
+
assert daily_risk > 0 if daily_risk is not None else ...
|
|
112
|
+
daily_risk = round(daily_risk, 5) if daily_risk is not None else None
|
|
113
|
+
assert all(isinstance(v, int) and v > 0 for v in [sl, tp] if v is not None)
|
|
114
|
+
assert isinstance(be, (int, float)) and be > 0 if be else ...
|
|
115
|
+
assert time_frame in TIMEFRAMES
|
|
116
|
+
|
|
117
|
+
self.kwargs = kwargs
|
|
118
|
+
self.symbol = symbol
|
|
119
|
+
self.timeframe = time_frame
|
|
120
|
+
self.start_time = start_time
|
|
121
|
+
self.finishing_time = finishing_time
|
|
122
|
+
self.max_trades = max_trades
|
|
123
|
+
self.std_stop = std_stop
|
|
124
|
+
self.pchange = pchange_sl
|
|
125
|
+
self.act_leverage = accountt_leverage
|
|
126
|
+
self.daily_dd = daily_risk
|
|
127
|
+
self.max_risk = max_risk
|
|
128
|
+
self.broker_tz = broker_tz
|
|
129
|
+
self.rr = rr
|
|
130
|
+
self.sl = sl
|
|
131
|
+
self.tp = tp
|
|
132
|
+
self.be = be
|
|
133
|
+
|
|
134
|
+
self.account = Account(**kwargs)
|
|
135
|
+
self.symbol_info = client.symbol_info(self.symbol)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def dailydd(self) -> float:
|
|
139
|
+
return self.daily_dd
|
|
140
|
+
|
|
141
|
+
@dailydd.setter
|
|
142
|
+
def dailydd(self, value: float):
|
|
143
|
+
self.daily_dd = value
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def maxrisk(self) -> float:
|
|
147
|
+
return self.max_risk
|
|
148
|
+
|
|
149
|
+
@maxrisk.setter
|
|
150
|
+
def maxrisk(self, value: float):
|
|
151
|
+
self.max_risk = value
|
|
152
|
+
|
|
153
|
+
def _convert_time_frame(self, timeframe: str) -> int:
|
|
154
|
+
"""Convert time frame to minutes"""
|
|
155
|
+
if timeframe == "D1":
|
|
156
|
+
return self.get_minutes()
|
|
157
|
+
elif "m" in timeframe:
|
|
158
|
+
return TIMEFRAMES[timeframe]
|
|
159
|
+
elif "h" in timeframe:
|
|
160
|
+
return int(timeframe[0]) * 60
|
|
161
|
+
elif timeframe == "W1":
|
|
162
|
+
return self.get_minutes() * 5
|
|
163
|
+
elif timeframe == "MN1":
|
|
164
|
+
return self.get_minutes() * 22
|
|
165
|
+
|
|
166
|
+
def get_minutes(self) -> int:
|
|
167
|
+
"""calculates the number of minutes between
|
|
168
|
+
the starting of the session and the end of the session"""
|
|
169
|
+
|
|
170
|
+
fmt = "%H:%M"
|
|
171
|
+
start = datetime.strptime(self.start_time, fmt)
|
|
172
|
+
end = datetime.strptime(self.finishing_time, fmt)
|
|
173
|
+
if self.broker_tz:
|
|
174
|
+
start = self.account.broker.get_broker_time(self.start_time, fmt)
|
|
175
|
+
end = self.account.broker.get_broker_time(self.finishing_time, fmt)
|
|
176
|
+
diff = (end - start).total_seconds()
|
|
177
|
+
diff += 86400 if diff < 0 else diff
|
|
178
|
+
return int(diff // 60)
|
|
179
|
+
|
|
180
|
+
def get_hours(self) -> int:
|
|
181
|
+
"""Calculates the number of hours between
|
|
182
|
+
the starting of the session and the end of the session"""
|
|
183
|
+
return self.get_minutes() // 60
|
|
184
|
+
|
|
185
|
+
def risk_level(self, balance_value=False) -> float | Tuple[float, float]:
|
|
186
|
+
"""
|
|
187
|
+
Calculates the risk level of a trade
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
- Risk level in the form of a float percentage.
|
|
191
|
+
"""
|
|
192
|
+
account_info = self.account.get_account_info()
|
|
193
|
+
balance = account_info.balance
|
|
194
|
+
equity = account_info.equity
|
|
195
|
+
if equity == 0:
|
|
196
|
+
return 0.0
|
|
197
|
+
trades_history = self.account.get_trades_history()
|
|
198
|
+
|
|
199
|
+
realized_profit = None
|
|
200
|
+
if trades_history is None or len(trades_history) == 1:
|
|
201
|
+
realized_profit = 0
|
|
202
|
+
else:
|
|
203
|
+
profit_df = trades_history.iloc[1:]
|
|
204
|
+
profit = profit_df["profit"].sum()
|
|
205
|
+
commisions = trades_history["commission"].sum()
|
|
206
|
+
fees = trades_history["fee"].sum()
|
|
207
|
+
swap = trades_history["swap"].sum()
|
|
208
|
+
realized_profit = commisions + fees + swap + profit
|
|
209
|
+
|
|
210
|
+
initial_balance = balance - realized_profit
|
|
211
|
+
dd_percent = ((equity - initial_balance) / equity) * 100
|
|
212
|
+
dd_percent = round(abs(dd_percent) if dd_percent < 0 else 0.0, 2)
|
|
213
|
+
if balance_value:
|
|
214
|
+
return (initial_balance, equity)
|
|
215
|
+
return dd_percent
|
|
216
|
+
|
|
217
|
+
def _get_lot(self) -> float:
|
|
218
|
+
lot = self.currency_risk()["lot"]
|
|
219
|
+
return self.account.broker.validate_lot_size(self.symbol, lot)
|
|
220
|
+
|
|
221
|
+
def get_lot(self) -> float:
|
|
222
|
+
return self.validate_currency_risk()[0]
|
|
223
|
+
|
|
224
|
+
def max_trade(self) -> int:
|
|
225
|
+
"""calculates the maximum number of trades allowed"""
|
|
226
|
+
minutes = self.get_minutes()
|
|
227
|
+
tf_int = self._convert_time_frame(self.timeframe)
|
|
228
|
+
max_trades = self.max_trades or round(minutes / tf_int)
|
|
229
|
+
return max(max_trades, 1)
|
|
230
|
+
|
|
231
|
+
def get_deviation(self) -> int:
|
|
232
|
+
return client.symbol_info(self.symbol).spread
|
|
233
|
+
|
|
234
|
+
def _get_stop(self, pchange: float) -> int:
|
|
235
|
+
tick = client.symbol_info_tick(self.symbol)
|
|
236
|
+
av_price = (tick.bid + tick.ask) / 2
|
|
237
|
+
price_interval = av_price * (100 - pchange) / 100
|
|
238
|
+
point = self.symbol_info.point
|
|
239
|
+
sl = round(float((av_price - price_interval) / point))
|
|
240
|
+
min_sl = (
|
|
241
|
+
self.account.broker.get_min_stop_level(self.symbol) * 2
|
|
242
|
+
+ self.get_deviation()
|
|
243
|
+
)
|
|
244
|
+
return max(sl, min_sl)
|
|
245
|
+
|
|
246
|
+
def _get_returns(self):
|
|
247
|
+
minutes = self.get_minutes()
|
|
248
|
+
tf_int = self._convert_time_frame(self.timeframe)
|
|
249
|
+
interval = round((minutes / tf_int) * 252)
|
|
250
|
+
rates = client.copy_rates_from_pos(
|
|
251
|
+
self.symbol, TIMEFRAMES[self.timeframe], 0, interval
|
|
252
|
+
)
|
|
253
|
+
returns = (np.diff(rates["close"]) / rates["close"][:-1]) * 100
|
|
254
|
+
return returns
|
|
255
|
+
|
|
256
|
+
def get_std_stop(self) -> int:
|
|
257
|
+
"""
|
|
258
|
+
Calculate the standard deviation-based stop loss level
|
|
259
|
+
for a given financial instrument.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
- Standard deviation-based stop loss level, rounded to the nearest point.
|
|
263
|
+
- 0 if the calculated stop loss is less than or equal to 0.
|
|
264
|
+
"""
|
|
265
|
+
std = np.std(self._get_returns())
|
|
266
|
+
return self._get_stop(std)
|
|
267
|
+
|
|
268
|
+
def get_pchange_stop(self, pchange: Optional[float]) -> int:
|
|
269
|
+
"""
|
|
270
|
+
Calculate the percentage change-based stop loss level
|
|
271
|
+
for a given financial instrument.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
pchange (float): Percentage change in price to use for calculating stop loss level.
|
|
275
|
+
If pchange is set to None, the stop loss is calculate using std.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
- Percentage change-based stop loss level, rounded to the nearest point.
|
|
279
|
+
- 0 if the calculated stop loss is <= 0.
|
|
280
|
+
"""
|
|
281
|
+
if pchange is not None:
|
|
282
|
+
return self._get_stop(pchange)
|
|
283
|
+
else:
|
|
284
|
+
# Use std as default pchange
|
|
285
|
+
return self.get_std_stop()
|
|
286
|
+
|
|
287
|
+
def calculate_var(self, tf: TimeFrame = "D1", c=0.95) -> float:
|
|
288
|
+
"""
|
|
289
|
+
Calculate Value at Risk (VaR) for a given portfolio.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
tf (str): Time frame to use to calculate volatility.
|
|
293
|
+
c (float): Confidence level for VaR calculation (default is 95%).
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
- VaR value
|
|
297
|
+
"""
|
|
298
|
+
returns = self._get_returns()
|
|
299
|
+
P = self.account.get_account_info().margin_free
|
|
300
|
+
mu = returns.mean()
|
|
301
|
+
sigma = returns.std()
|
|
302
|
+
alpha = norm.ppf(1 - c, mu, sigma)
|
|
303
|
+
return P - P * (alpha + 1)
|
|
304
|
+
|
|
305
|
+
def get_trade_risk(self) -> float:
|
|
306
|
+
"""Calculate risk per trade as percentage"""
|
|
307
|
+
total_risk = self.risk_level()
|
|
308
|
+
max_trades = self.max_trade()
|
|
309
|
+
if total_risk < self.max_risk:
|
|
310
|
+
if self.daily_dd is not None:
|
|
311
|
+
trade_risk = self.daily_dd / max_trades
|
|
312
|
+
else:
|
|
313
|
+
trade_risk = (self.max_risk - total_risk) / max_trades
|
|
314
|
+
return trade_risk
|
|
315
|
+
else:
|
|
316
|
+
return 0
|
|
317
|
+
|
|
318
|
+
def var_loss_value(self) -> float:
|
|
319
|
+
"""
|
|
320
|
+
Calculate the stop-loss level based on VaR.
|
|
321
|
+
|
|
322
|
+
Notes:
|
|
323
|
+
The Var is Estimated using the Variance-Covariance method on the daily returns.
|
|
324
|
+
If you want to use the VaR for a different time frame .
|
|
325
|
+
"""
|
|
326
|
+
P = self.account.get_account_info().margin_free
|
|
327
|
+
trade_risk = self.get_trade_risk()
|
|
328
|
+
loss_allowed = P * trade_risk / 100
|
|
329
|
+
var = self.calculate_var()
|
|
330
|
+
return min(var, loss_allowed)
|
|
331
|
+
|
|
332
|
+
def get_take_profit(self) -> int:
|
|
333
|
+
"""calculates the take profit of a trade in points"""
|
|
334
|
+
deviation = self.get_deviation()
|
|
335
|
+
if self.tp is not None:
|
|
336
|
+
return self.tp + deviation
|
|
337
|
+
else:
|
|
338
|
+
return round(self.get_stop_loss() * self.rr)
|
|
339
|
+
|
|
340
|
+
def _get_stop_loss(self) -> int:
|
|
341
|
+
"""calculates the stop loss of a trade in points"""
|
|
342
|
+
min_sl = (
|
|
343
|
+
self.account.broker.get_min_stop_level(self.symbol) * 2
|
|
344
|
+
+ self.get_deviation()
|
|
345
|
+
)
|
|
346
|
+
if self.sl is not None:
|
|
347
|
+
return max(self.sl, min_sl)
|
|
348
|
+
if self.std_stop:
|
|
349
|
+
sl = self.get_std_stop()
|
|
350
|
+
return max(sl, min_sl)
|
|
351
|
+
if self.pchange is not None:
|
|
352
|
+
sl = self.get_pchange_stop(self.pchange)
|
|
353
|
+
return max(sl, min_sl)
|
|
354
|
+
risk = self.currency_risk()
|
|
355
|
+
if risk["trade_loss"] != 0:
|
|
356
|
+
sl = round(risk["currency_risk"] / risk["trade_loss"])
|
|
357
|
+
return max(sl, min_sl)
|
|
358
|
+
return min_sl
|
|
359
|
+
|
|
360
|
+
def get_stop_loss(self) -> float:
|
|
361
|
+
return self.validate_currency_risk()[1]
|
|
362
|
+
|
|
363
|
+
def get_currency_risk(self) -> float:
|
|
364
|
+
"""calculates the currency risk of a trade"""
|
|
365
|
+
return round(self.currency_risk()["currency_risk"], 2)
|
|
366
|
+
|
|
367
|
+
def expected_profit(self):
|
|
368
|
+
"""Calculate the expected profit per trade"""
|
|
369
|
+
risk = self.get_currency_risk()
|
|
370
|
+
return round(risk * self.rr, 2)
|
|
371
|
+
|
|
372
|
+
def volume(self):
|
|
373
|
+
"""Volume per trade"""
|
|
374
|
+
|
|
375
|
+
return self.currency_risk()["volume"]
|
|
376
|
+
|
|
377
|
+
def _std_pchange_stop(self, currency_risk, sl, size, loss):
|
|
378
|
+
trade_loss = currency_risk / sl if sl != 0 else 0.0
|
|
379
|
+
trade_profit = (currency_risk * self.rr) / (sl * self.rr) if sl != 0 else 0.0
|
|
380
|
+
av_price = (self.symbol_info.bid + self.symbol_info.ask) / 2
|
|
381
|
+
lot = round(trade_loss / (size * loss), 2) if size * loss != 0 else 0.0
|
|
382
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
383
|
+
volume = round(lot * size * av_price)
|
|
384
|
+
if self.account.get_symbol_type(self.symbol) == SymbolType.FOREX:
|
|
385
|
+
volume = round(trade_loss * size / loss) if loss != 0 else 0
|
|
386
|
+
lot = round(volume / size, 2) if size != 0 else 0.0
|
|
387
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
388
|
+
if (
|
|
389
|
+
self.account.get_symbol_type(self.symbol)
|
|
390
|
+
in [SymbolType.COMMODITIES, SymbolType.CRYPTO]
|
|
391
|
+
and size > 1
|
|
392
|
+
):
|
|
393
|
+
lot = currency_risk / (sl * loss * size) if sl * loss * size != 0 else 0.0
|
|
394
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
395
|
+
trade_loss = lot * size * loss
|
|
396
|
+
volume = round(lot * size * av_price)
|
|
397
|
+
return trade_loss, trade_profit, lot, volume
|
|
398
|
+
|
|
399
|
+
def currency_risk(self) -> Dict[str, Union[int, float, Any]]:
|
|
400
|
+
"""
|
|
401
|
+
Calculates the currency risk of a trade.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Dict[str, Union[int, float, Any]]: A dictionary containing the following keys:
|
|
405
|
+
|
|
406
|
+
- `'currency_risk'`: Dollar amount risk on a single trade.
|
|
407
|
+
- `'trade_loss'`: Loss value per tick in dollars.
|
|
408
|
+
- `'trade_profit'`: Profit value per tick in dollars.
|
|
409
|
+
- `'volume'`: Contract size multiplied by the average price.
|
|
410
|
+
- `'lot'`: Lot size per trade.
|
|
411
|
+
"""
|
|
412
|
+
s_info = self.account.get_symbol_info(self.symbol)
|
|
413
|
+
leverage = self.account.broker.get_leverage_for_symbol(
|
|
414
|
+
self.symbol, self.act_leverage
|
|
415
|
+
)
|
|
416
|
+
contract_size = s_info.trade_contract_size
|
|
417
|
+
av_price = (s_info.bid + s_info.ask) / 2
|
|
418
|
+
trade_risk = self.get_trade_risk()
|
|
419
|
+
symbol_type = self.account.get_symbol_type(self.symbol)
|
|
420
|
+
|
|
421
|
+
tick_value_loss, tick_value_profit = self.account.broker.adjust_tick_values(
|
|
422
|
+
self.symbol,
|
|
423
|
+
s_info.trade_tick_value_loss,
|
|
424
|
+
s_info.trade_tick_value_profit,
|
|
425
|
+
contract_size,
|
|
426
|
+
)
|
|
427
|
+
tick_value = s_info.trade_tick_value # For checks
|
|
428
|
+
|
|
429
|
+
if tick_value == 0 or tick_value_loss == 0 or tick_value_profit == 0:
|
|
430
|
+
logger.error(
|
|
431
|
+
f"The Tick Values for {self.symbol} is 0.0. Check broker conditions for {self.symbol}."
|
|
432
|
+
)
|
|
433
|
+
return {
|
|
434
|
+
"currency_risk": 0.0,
|
|
435
|
+
"trade_loss": 0.0,
|
|
436
|
+
"trade_profit": 0.0,
|
|
437
|
+
"volume": 0,
|
|
438
|
+
"lot": 0.01,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if trade_risk > 0:
|
|
442
|
+
currency_risk = round(self.var_loss_value(), 5)
|
|
443
|
+
volume = round(currency_risk * leverage)
|
|
444
|
+
lot = (
|
|
445
|
+
round(volume / (contract_size * av_price), 2)
|
|
446
|
+
if contract_size * av_price != 0
|
|
447
|
+
else 0.0
|
|
448
|
+
)
|
|
449
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
450
|
+
|
|
451
|
+
if symbol_type == SymbolType.COMMODITIES and contract_size > 1:
|
|
452
|
+
lot = (
|
|
453
|
+
volume / (av_price * contract_size)
|
|
454
|
+
if av_price * contract_size != 0
|
|
455
|
+
else 0.0
|
|
456
|
+
)
|
|
457
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
458
|
+
if symbol_type == SymbolType.FOREX:
|
|
459
|
+
lot = round(volume / contract_size, 2) if contract_size != 0 else 0.0
|
|
460
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
461
|
+
|
|
462
|
+
if self.sl is not None:
|
|
463
|
+
trade_loss = currency_risk / self.sl if self.sl != 0 else 0.0
|
|
464
|
+
trade_profit = (
|
|
465
|
+
(currency_risk * (self.tp // self.sl if self.tp else self.rr))
|
|
466
|
+
/ (self.tp or (self.sl * self.rr))
|
|
467
|
+
if self.sl != 0
|
|
468
|
+
else 0.0
|
|
469
|
+
)
|
|
470
|
+
lot = (
|
|
471
|
+
round(trade_loss / (contract_size * tick_value_loss), 2)
|
|
472
|
+
if contract_size * tick_value_loss != 0
|
|
473
|
+
else 0.0
|
|
474
|
+
)
|
|
475
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
476
|
+
volume = round(lot * contract_size * av_price)
|
|
477
|
+
|
|
478
|
+
if (
|
|
479
|
+
symbol_type in [SymbolType.COMMODITIES, SymbolType.CRYPTO]
|
|
480
|
+
) and contract_size > 1:
|
|
481
|
+
lot = (
|
|
482
|
+
currency_risk / (self.sl * tick_value_loss * contract_size)
|
|
483
|
+
if self.sl * tick_value_loss * contract_size != 0
|
|
484
|
+
else 0.0
|
|
485
|
+
)
|
|
486
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
487
|
+
trade_loss = lot * contract_size * tick_value_loss
|
|
488
|
+
|
|
489
|
+
if symbol_type == SymbolType.FOREX:
|
|
490
|
+
volume = (
|
|
491
|
+
round(trade_loss * contract_size / tick_value_loss)
|
|
492
|
+
if tick_value_loss != 0
|
|
493
|
+
else 0
|
|
494
|
+
)
|
|
495
|
+
lot = (
|
|
496
|
+
round(volume / contract_size, 2) if contract_size != 0 else 0.0
|
|
497
|
+
)
|
|
498
|
+
lot = self.account.broker.validate_lot_size(self.symbol, lot)
|
|
499
|
+
|
|
500
|
+
elif self.std_stop and self.pchange is None and self.sl is None:
|
|
501
|
+
sl = self.get_std_stop()
|
|
502
|
+
trade_loss, trade_profit, lot, volume = self._std_pchange_stop(
|
|
503
|
+
currency_risk, sl, contract_size, tick_value_loss
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
elif self.pchange is not None and not self.std_stop and self.sl is None:
|
|
507
|
+
sl = self.get_pchange_stop(self.pchange)
|
|
508
|
+
trade_loss, trade_profit, lot, volume = self._std_pchange_stop(
|
|
509
|
+
currency_risk, sl, contract_size, tick_value_loss
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
else:
|
|
513
|
+
if symbol_type == SymbolType.FOREX:
|
|
514
|
+
trade_loss = (
|
|
515
|
+
tick_value_loss * (volume / contract_size)
|
|
516
|
+
if contract_size != 0
|
|
517
|
+
else 0.0
|
|
518
|
+
)
|
|
519
|
+
trade_profit = (
|
|
520
|
+
tick_value_profit * (volume / contract_size)
|
|
521
|
+
if contract_size != 0
|
|
522
|
+
else 0.0
|
|
523
|
+
)
|
|
524
|
+
else:
|
|
525
|
+
trade_loss = (lot * contract_size) * tick_value_loss
|
|
526
|
+
trade_profit = (lot * contract_size) * tick_value_profit
|
|
527
|
+
|
|
528
|
+
# Apply currency conversion
|
|
529
|
+
rates = self.account.get_currency_rates(self.symbol)
|
|
530
|
+
factor = self.account.broker.get_currency_conversion_factor(
|
|
531
|
+
self.symbol, rates.get("pc", ""), self.account.currency
|
|
532
|
+
)
|
|
533
|
+
trade_profit *= factor
|
|
534
|
+
trade_loss *= factor
|
|
535
|
+
currency_risk *= factor
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
"currency_risk": currency_risk,
|
|
539
|
+
"trade_loss": trade_loss,
|
|
540
|
+
"trade_profit": trade_profit,
|
|
541
|
+
"volume": round(volume),
|
|
542
|
+
"lot": lot,
|
|
543
|
+
}
|
|
544
|
+
else:
|
|
545
|
+
return {
|
|
546
|
+
"currency_risk": 0.0,
|
|
547
|
+
"trade_loss": 0.0,
|
|
548
|
+
"trade_profit": 0.0,
|
|
549
|
+
"volume": 0,
|
|
550
|
+
"lot": 0.01,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
def validate_currency_risk(self):
|
|
554
|
+
target_risk = self.get_currency_risk()
|
|
555
|
+
sl_points = self._get_stop_loss()
|
|
556
|
+
start_lot = self._get_lot()
|
|
557
|
+
|
|
558
|
+
tick = client.symbol_info_tick(self.symbol)
|
|
559
|
+
if tick is None:
|
|
560
|
+
logger.error(f"No tick for {self.symbol}. Validation failed.")
|
|
561
|
+
return 0.0, sl_points
|
|
562
|
+
|
|
563
|
+
ask = tick.ask
|
|
564
|
+
sl_price = ask - (sl_points * self.symbol_info.point)
|
|
565
|
+
|
|
566
|
+
balance, equity = self.risk_level(balance_value=True)
|
|
567
|
+
margin_free = self.account.get_account_info().margin_free
|
|
568
|
+
allowed_drawdown = margin_free * (self.max_risk / 100)
|
|
569
|
+
min_equity = balance - allowed_drawdown
|
|
570
|
+
max_safe_loss = equity - min_equity
|
|
571
|
+
|
|
572
|
+
if max_safe_loss <= 0:
|
|
573
|
+
logger.warning(
|
|
574
|
+
f"Equity ({equity}$) below safety threshold ({min_equity}$)."
|
|
575
|
+
)
|
|
576
|
+
return 0.0, sl_points
|
|
577
|
+
min_vol = self.symbol_info.volume_min
|
|
578
|
+
min_loss = client.order_calc_profit(
|
|
579
|
+
mt5.ORDER_TYPE_BUY, self.symbol, min_vol, ask, sl_price
|
|
580
|
+
)
|
|
581
|
+
if min_loss is None or min_loss >= 0:
|
|
582
|
+
logger.warning(
|
|
583
|
+
f"Invalid min loss calculation for {self.symbol}: {min_loss}"
|
|
584
|
+
)
|
|
585
|
+
return 0.0, sl_points
|
|
586
|
+
min_loss = abs(min_loss)
|
|
587
|
+
if min_loss > max_safe_loss:
|
|
588
|
+
logger.error(
|
|
589
|
+
f"CRITICAL: Min vol loss ({min_loss:.2f}$) exceeds max safe loss ({max_safe_loss:.2f}$)."
|
|
590
|
+
)
|
|
591
|
+
return 0.0, sl_points
|
|
592
|
+
effective_risk = min(target_risk, max_safe_loss)
|
|
593
|
+
start_loss = client.order_calc_profit(
|
|
594
|
+
mt5.ORDER_TYPE_BUY, self.symbol, start_lot, ask, sl_price
|
|
595
|
+
)
|
|
596
|
+
if start_loss is None or start_loss >= 0:
|
|
597
|
+
base_vol, base_loss = min_vol, min_loss
|
|
598
|
+
else:
|
|
599
|
+
base_vol, base_loss = start_lot, abs(start_loss)
|
|
600
|
+
if base_loss == 0:
|
|
601
|
+
vol = min_vol
|
|
602
|
+
else:
|
|
603
|
+
ratio = effective_risk / base_loss
|
|
604
|
+
calc_vol = base_vol * ratio
|
|
605
|
+
|
|
606
|
+
step = self.symbol_info.volume_step
|
|
607
|
+
vol = round(calc_vol / step) * step
|
|
608
|
+
vol = self.account.broker.validate_lot_size(self.symbol, vol)
|
|
609
|
+
final_loss = client.order_calc_profit(
|
|
610
|
+
mt5.ORDER_TYPE_BUY, self.symbol, vol, ask, sl_price
|
|
611
|
+
)
|
|
612
|
+
if final_loss is None or final_loss >= 0:
|
|
613
|
+
return 0.0, sl_points
|
|
614
|
+
final_loss = abs(final_loss)
|
|
615
|
+
|
|
616
|
+
if final_loss > max_safe_loss:
|
|
617
|
+
vol_down = max(min_vol, vol - step)
|
|
618
|
+
loss_down = client.order_calc_profit(
|
|
619
|
+
mt5.ORDER_TYPE_BUY, self.symbol, vol_down, ask, sl_price
|
|
620
|
+
)
|
|
621
|
+
if loss_down is not None and abs(loss_down) <= max_safe_loss:
|
|
622
|
+
vol = vol_down
|
|
623
|
+
else:
|
|
624
|
+
return 0.0, sl_points
|
|
625
|
+
return (vol, round(sl_points)) if vol > 0 else (0.0, sl_points)
|
|
626
|
+
|
|
627
|
+
def get_break_even(self, thresholds: list[tuple[int, float]] = None) -> int:
|
|
628
|
+
"""
|
|
629
|
+
Calculates the break-even price level based on stop-loss tiers.
|
|
630
|
+
|
|
631
|
+
The function determines the break-even point by applying a multiplier to the
|
|
632
|
+
sum of the current stop-loss and market spread. If an explicit break-even
|
|
633
|
+
value (`self.be`) is already set, it returns that value (converting
|
|
634
|
+
percentage-based floats to absolute points if necessary).
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
thresholds (list[tuple[int, float]], optional): A list of tiers defined
|
|
638
|
+
as (threshold_limit, multiplier).
|
|
639
|
+
Example: [(150, 0.25), (100, 0.35), (0, 0.5)].
|
|
640
|
+
If None, defaults to standard conservative tiers.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
int: The calculated break-even value in points/pips.
|
|
644
|
+
|
|
645
|
+
Note:
|
|
646
|
+
The function automatically sorts thresholds in descending order to
|
|
647
|
+
ensure the 'stop' value is matched against the highest possible tier first.
|
|
648
|
+
"""
|
|
649
|
+
if self.be is not None:
|
|
650
|
+
return (
|
|
651
|
+
self.be if isinstance(self.be, int) else self.get_pchange_stop(self.be)
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
if thresholds is None:
|
|
655
|
+
thresholds = [(150, 0.25), (100, 0.35), (0, 0.50)]
|
|
656
|
+
|
|
657
|
+
stop = self.get_stop_loss()
|
|
658
|
+
spread = client.symbol_info(self.symbol).spread
|
|
659
|
+
sorted_thresholds = sorted(thresholds, key=lambda x: x[0], reverse=True)
|
|
660
|
+
|
|
661
|
+
for limit, multiplier in sorted_thresholds:
|
|
662
|
+
if stop > limit:
|
|
663
|
+
return round((stop + spread) * multiplier)
|
|
664
|
+
return 0
|
|
665
|
+
|
|
666
|
+
def is_risk_ok(self) -> bool:
|
|
667
|
+
return self.risk_level() <= self.max_risk
|