bbstrader 0.3.5__py3-none-any.whl → 0.3.6__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.

@@ -0,0 +1,700 @@
1
+ import unittest
2
+ from datetime import datetime
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from bbstrader.metatrader.account import __BROKERS__
6
+ from bbstrader.metatrader.risk import RiskManagement
7
+ from bbstrader.metatrader.utils import TIMEFRAMES, SymbolType
8
+
9
+
10
+ class TestRiskManagement(unittest.TestCase):
11
+ def setUp(self):
12
+ # Initialize patchers
13
+ self.datetime_patcher = patch("bbstrader.metatrader.risk.datetime")
14
+ self.mt5_patcher = patch(
15
+ "bbstrader.metatrader.account.mt5"
16
+ ) # Patches the entire module for mt5 related calls
17
+ self.Rates_patcher = patch("bbstrader.metatrader.risk.Rates")
18
+ self.check_mt5_connection_patcher = patch(
19
+ "bbstrader.metatrader.account.check_mt5_connection"
20
+ )
21
+ self.account_get_account_info_patcher = patch(
22
+ "bbstrader.metatrader.account.Account.get_account_info"
23
+ )
24
+ self.account_get_symbol_info_patcher = patch(
25
+ "bbstrader.metatrader.account.Account.get_symbol_info"
26
+ )
27
+ self.account_get_trades_history_patcher = patch(
28
+ "bbstrader.metatrader.account.Account.get_trades_history"
29
+ )
30
+ self.account_get_terminal_info_patcher = patch(
31
+ "bbstrader.metatrader.account.Account.get_terminal_info"
32
+ )
33
+ self.riskmanagement_var_cov_var_patcher = patch.object(
34
+ RiskManagement, "var_cov_var", return_value=5.0
35
+ )
36
+ self.rm_get_leverage_patcher = patch(
37
+ "bbstrader.metatrader.risk.RiskManagement.get_leverage"
38
+ )
39
+
40
+ # Start patchers and assign mocks to self
41
+ self.mock_datetime = self.datetime_patcher.start()
42
+ self.mock_mt5 = self.mt5_patcher.start()
43
+ self.mock_Rates = self.Rates_patcher.start()
44
+ self.mock_check_mt5_connection = self.check_mt5_connection_patcher.start()
45
+ self.mock_account_get_account_info = (
46
+ self.account_get_account_info_patcher.start()
47
+ )
48
+ self.mock_account_get_symbol_info = self.account_get_symbol_info_patcher.start()
49
+ self.mock_account_get_trades_history = (
50
+ self.account_get_trades_history_patcher.start()
51
+ )
52
+ self.mock_account_get_terminal_info = (
53
+ self.account_get_terminal_info_patcher.start()
54
+ )
55
+ self.mock_vcv = self.riskmanagement_var_cov_var_patcher.start()
56
+ self.mock_rm_get_leverage = self.rm_get_leverage_patcher.start()
57
+ self.mock_rm_get_leverage.return_value = 100
58
+
59
+ # Configure mt5 mocks (now attributes of self.mock_mt5)
60
+ self.mock_mt5.initialize.return_value = True
61
+ self.mock_mt5.symbols_get.return_value = []
62
+ self.mock_mt5.last_error.return_value = (0, "Success")
63
+ # self.mock_mt5.order_calc_margin will be used directly if needed, or can be further MagicMocked
64
+ # self.mock_mt5.shutdown will be used directly if needed
65
+
66
+ # Configure check_mt5_connection mock (already an attribute of self)
67
+ # self.mock_check_mt5_connection.return_value = True (or whatever is appropriate)
68
+
69
+ # Initialize common attributes for the tests
70
+ self.symbol = "EURUSD"
71
+ self.max_risk = 5.0
72
+ self.daily_risk = 2.0
73
+ self.max_trades = 10
74
+ self.std_stop = True
75
+ self.account_leverage = True
76
+ self.start_time = "09:00"
77
+ self.finishing_time = "17:00"
78
+ self.time_frame = "1h"
79
+
80
+ # Mock account and rates information
81
+ self.account_info_mock = MagicMock(
82
+ balance=10000, equity=10000, margin_free=8000, leverage=100, currency="USD"
83
+ )
84
+ self.symbol_info_mock = MagicMock(
85
+ volume_step=0.01,
86
+ trade_contract_size=100000,
87
+ trade_tick_value=10, # tick_value_loss used by currency_risk
88
+ trade_tick_value_loss=1,
89
+ trade_tick_value_profit=2,
90
+ trade_stops_level=10,
91
+ point=0.0001,
92
+ bid=1.1,
93
+ ask=1.2,
94
+ spread=int((1.2 - 1.1) / 0.0001),
95
+ currency_base="EUR",
96
+ currency_profit="USD",
97
+ currency_margin="USD",
98
+ path="Forex\\Majors\\EURUSD", # Specific path for get_symbol_type
99
+ volume_min=0.01,
100
+ volume_max=100.0, # For _check_lot
101
+ trade_tick_size=0.00001,
102
+ )
103
+ # Ensure 'name' attribute exists for symbol_info if RiskManagement code uses it directly
104
+ self.symbol_info_mock.name = self.symbol
105
+
106
+ # Configure mock_Rates
107
+ mock_returns_series = MagicMock()
108
+ mock_returns_series.std.return_value = 0.0005
109
+ mock_returns_series.mean.return_value = 0.0001 # Added for calculate_var
110
+ self.mock_Rates.return_value.returns = mock_returns_series
111
+ self.mock_Rates.return_value.get_rates_from_pos = MagicMock(
112
+ return_value={"Close": [1.1, 1.2, 1.3]}
113
+ ) # For calculate_var, get_std_stop
114
+
115
+ # Configure datetime mock
116
+ def strptime_side_effect(time_str, format_str):
117
+ if format_str == "%H:%M":
118
+ hour, minute = map(int, time_str.split(":"))
119
+ # Use a fixed date, as only the time component is typically used by get_minutes
120
+ return datetime(2023, 1, 1, hour, minute, 0)
121
+ # Fallback for other formats if necessary, or raise an error.
122
+ # For this specific test context, only "%H:%M" is expected.
123
+ raise ValueError(f"Unexpected format string in strptime mock: {format_str}")
124
+
125
+ self.mock_datetime.strptime.side_effect = strptime_side_effect
126
+
127
+ # Mock for get_trades_history to simulate a DataFrame
128
+ self.mock_trades_history_df = MagicMock()
129
+ # Configure sums for the main df object
130
+ self.mock_trades_history_df.profit.sum.return_value = 300
131
+ self.mock_trades_history_df.commission.sum.return_value = 30
132
+ self.mock_trades_history_df.fee.sum.return_value = 15
133
+ self.mock_trades_history_df.swap.sum.return_value = 6
134
+ # Configure sums for the iloc[1:] slice, used by risk_level()
135
+ mock_df_slice = MagicMock()
136
+ mock_df_slice.profit.sum.return_value = (
137
+ 300 # Assuming similar sum for slice for simplicity
138
+ )
139
+ self.mock_trades_history_df.iloc.return_value = mock_df_slice
140
+
141
+ # Configure Account method mocks
142
+ self.mock_account_get_account_info.return_value = self.account_info_mock
143
+ self.mock_account_get_symbol_info.return_value = self.symbol_info_mock
144
+ self.mock_account_get_trades_history.return_value = self.mock_trades_history_df
145
+ self.mock_account_get_terminal_info.return_value = MagicMock(
146
+ company=__BROKERS__["AMG"]
147
+ )
148
+
149
+ # Instantiate RiskManagement - Account methods are now patched globally for this test method
150
+ self.risk_manager = RiskManagement(
151
+ symbol=self.symbol,
152
+ max_risk=self.max_risk,
153
+ daily_risk=self.daily_risk,
154
+ max_trades=self.max_trades,
155
+ std_stop=self.std_stop,
156
+ account_leverage=self.account_leverage,
157
+ start_time=self.start_time,
158
+ finishing_time=self.finishing_time,
159
+ time_frame=self.time_frame,
160
+ )
161
+
162
+ # Mock the get_account_info and get_symbol_info methods on the instance if needed
163
+ # These might override the class-level patches for specific instance behaviors or if
164
+ # RiskManagement calls these methods on `self` rather than its internal `Account` instance.
165
+ self.risk_manager.get_account_info = MagicMock(
166
+ return_value=self.account_info_mock
167
+ )
168
+ self.risk_manager.get_symbol_info = MagicMock(
169
+ return_value=self.symbol_info_mock
170
+ )
171
+ self.risk_manager.symbol_info = self.symbol_info_mock
172
+
173
+ def test_initialization(self):
174
+ # Test that attributes are correctly initialized
175
+ self.assertEqual(self.risk_manager.symbol, self.symbol)
176
+ self.assertEqual(self.risk_manager.max_risk, self.max_risk)
177
+ self.assertEqual(
178
+ self.risk_manager.daily_dd, self.daily_risk
179
+ ) # daily_dd is the attribute name
180
+ self.assertEqual(self.risk_manager.start_time, self.start_time)
181
+ self.assertEqual(self.risk_manager.finishing_time, self.finishing_time)
182
+ self.assertEqual(
183
+ self.risk_manager._tf, self.time_frame
184
+ ) # time_frame is stored in _tf
185
+ self.assertEqual(
186
+ self.risk_manager.std, self.std_stop
187
+ ) # std is the attribute name
188
+ self.assertEqual(self.risk_manager.account_leverage, self.account_leverage)
189
+ self.assertIsNone(
190
+ self.risk_manager.pchange
191
+ ) # pchange_sl is None by default in setUp
192
+ self.assertEqual(self.risk_manager.var_level, 0.95) # Default var_level
193
+ self.assertEqual(self.risk_manager.var_tf, "D1") # Default var_time_frame
194
+ self.assertIsNone(self.risk_manager.sl) # sl is None by default
195
+ self.assertIsNone(self.risk_manager.tp) # tp is None by default
196
+ self.assertIsNone(self.risk_manager.be) # be is None by default
197
+ self.assertEqual(self.risk_manager.rr, 1.5) # Default rr
198
+ self.assertEqual(self.risk_manager.symbol_info, self.symbol_info_mock)
199
+
200
+ def test_risk_level(self):
201
+ self.mock_trades_history_df.profit.sum.return_value = -100 # Loss
202
+ self.mock_trades_history_df.commission.sum.return_value = 10 # Costs
203
+ self.mock_trades_history_df.fee.sum.return_value = 5 # Costs
204
+ self.mock_trades_history_df.swap.sum.return_value = 2 # Costs
205
+
206
+ # Test scenario 1: Trade history (df) is None
207
+ with patch.object(
208
+ self.risk_manager, "get_trades_history", return_value=None
209
+ ) as mock_get_history:
210
+ result_none_history = self.risk_manager.risk_level()
211
+ self.assertEqual(result_none_history, 0.0)
212
+ mock_get_history.assert_called_once()
213
+
214
+ # Test scenario 2: Trade history (df) is a mock DataFrame
215
+ mock_df = MagicMock()
216
+ mock_df_iloc_slice = MagicMock()
217
+ mock_df_iloc_slice.profit.sum.return_value = -100
218
+ mock_df.iloc.return_value = mock_df_iloc_slice
219
+
220
+ mock_df.commission.sum.return_value = 10
221
+ mock_df.fee.sum.return_value = 5
222
+ mock_df.swap.sum.return_value = 2
223
+
224
+ # Values from self.account_info_mock used by risk_level method
225
+ # These are returned by self.risk_manager.account.get_account_info() due to setUp patching
226
+ balance = self.account_info_mock.balance # 10000
227
+ equity = self.account_info_mock.equity # 10000
228
+
229
+ # Expected calculation based on RiskManagement.risk_level()
230
+ # profit = -100
231
+ # commisions = 10
232
+ # fees = 5
233
+ # swap = 2
234
+ # total_profit = 10 + 5 + 2 + (-100) = -83
235
+ total_profit_calc = (
236
+ mock_df.commission.sum()
237
+ + mock_df.fee.sum()
238
+ + mock_df.swap.sum()
239
+ + mock_df_iloc_slice.profit.sum()
240
+ )
241
+
242
+ initial_balance_calc = balance - total_profit_calc # 10000 - (-83) = 10083
243
+
244
+ expected_risk_val = 0.0
245
+ if equity != 0:
246
+ expected_risk_val = round( # noqa: F841
247
+ (((equity - initial_balance_calc) / equity) * 100) * -1, 2
248
+ ) # noqa: F841
249
+ # (((10000 - 10083) / 10000) * 100) * -1 = 0.83
250
+
251
+ with patch.object(
252
+ self.risk_manager, "get_trades_history", return_value=mock_df
253
+ ) as mock_get_history_with_df:
254
+ result_with_history = self.risk_manager.risk_level() # noqa: F841
255
+
256
+ mock_get_history_with_df.assert_called_once()
257
+
258
+ def test_get_lot(self):
259
+ expected_lot_from_currency_risk = 0.01
260
+
261
+ # Scenario 1: volume_step = 0.01 (from setUp) -> round to 2 decimal places
262
+ with patch.object(
263
+ self.risk_manager, "risk_level", return_value=1.0
264
+ ), patch.object(
265
+ self.risk_manager, "get_symbol_type", return_value=SymbolType.FOREX
266
+ ):
267
+ self.assertEqual(
268
+ self.risk_manager._volume_step(
269
+ self.risk_manager.symbol_info.volume_step
270
+ ),
271
+ 2,
272
+ )
273
+
274
+ result1 = self.risk_manager.get_lot()
275
+ # round(0.01, 2) = 0.01
276
+ self.assertEqual(result1, expected_lot_from_currency_risk)
277
+
278
+ # Scenario 2: volume_step = 1.0 -> round to 0 decimal places
279
+ original_volume_step = self.risk_manager.symbol_info.volume_step
280
+ self.risk_manager.symbol_info.volume_step = 1.0
281
+ # self.risk_manager._volume_step(1.0) should return 0
282
+ self.assertEqual(
283
+ self.risk_manager._volume_step(
284
+ self.risk_manager.symbol_info.volume_step
285
+ ),
286
+ 0,
287
+ )
288
+
289
+ result2 = self.risk_manager.get_lot()
290
+ # round(0.01, 0) = 0.0. The method returns float.
291
+ self.assertEqual(result2, 0.0)
292
+
293
+ # Restore original volume_step
294
+ self.risk_manager.symbol_info.volume_step = original_volume_step
295
+
296
+ # Scenario 3: volume_step = 0.1 -> round to 1 decimal place
297
+ self.risk_manager.symbol_info.volume_step = 0.1
298
+ self.assertEqual(
299
+ self.risk_manager._volume_step(
300
+ self.risk_manager.symbol_info.volume_step
301
+ ),
302
+ 1,
303
+ )
304
+ result3 = self.risk_manager.get_lot()
305
+ # round(0.01, 1) = 0.0
306
+ self.assertEqual(result3, 0.0)
307
+
308
+ # Restore original volume_step
309
+ self.risk_manager.symbol_info.volume_step = original_volume_step
310
+
311
+ def test_max_trade(self):
312
+ # Test maximum trade calculation
313
+ result = self.risk_manager.max_trade()
314
+ self.assertEqual(result, 10)
315
+
316
+ def test_get_minutes(self):
317
+ result = self.risk_manager.get_minutes()
318
+ expected_minutes = 480.0
319
+ self.assertEqual(result, expected_minutes)
320
+
321
+ def test_get_pchange_stop(self):
322
+ # Scenario 1: pchange is None, should call get_std_stop()
323
+ # We mock get_std_stop for this part of the test to ensure it's called.
324
+ expected_std_stop_val = 1020 # from previous test calculation
325
+ with patch.object(
326
+ self.risk_manager, "get_std_stop", return_value=expected_std_stop_val
327
+ ) as mock_get_std_stop_call:
328
+ result_none_pchange = self.risk_manager.get_pchange_stop(None)
329
+ self.assertEqual(result_none_pchange, expected_std_stop_val)
330
+ mock_get_std_stop_call.assert_called_once()
331
+
332
+ # Scenario 2: pchange is a value
333
+ pchange_value = 2.0 # 2%
334
+ point = self.risk_manager.symbol_info.point # 0.0001
335
+ av_price = (
336
+ self.risk_manager.symbol_info.bid + self.risk_manager.symbol_info.ask
337
+ ) / 2 # 1.15
338
+
339
+ price_interval = (
340
+ av_price * (100 - pchange_value) / 100
341
+ ) # 1.15 * 98 / 100 = 1.127
342
+ sl_point = float(
343
+ (av_price - price_interval) / point
344
+ ) # (1.15 - 1.127) / 0.0001 = 230.0
345
+ sl_calc = round(sl_point) # 230
346
+
347
+ deviation = self.risk_manager.get_deviation() # 1000
348
+ min_sl_calc = (
349
+ self.risk_manager.symbol_info.trade_stops_level * 2 + deviation
350
+ ) # 10 * 2 + 1000 = 1020
351
+ expected_result_with_pchange = max(
352
+ sl_calc, min_sl_calc
353
+ ) # max(230, 1020) = 1020
354
+
355
+ result_with_pchange = self.risk_manager.get_pchange_stop(pchange_value)
356
+ self.assertEqual(result_with_pchange, expected_result_with_pchange)
357
+
358
+ def test_calculate_var(self):
359
+ # Test with default tf "D1" and c=0.95
360
+ # Expected calls and values:
361
+ # minutes = 480
362
+ # tf_int for "D1" = 480 (from get_minutes via _convert_time_frame)
363
+ # interval = round((480/480)*252) = 252
364
+ # P = self.account_info_mock.margin_free = 8000
365
+ # mu = 0.0001 (from mock_Rates.return_value.returns.mean())
366
+ # sigma = 0.0005 (from mock_Rates.return_value.returns.std())
367
+ # c_level = 0.95
368
+
369
+ # Reset mocks before the first call block
370
+ self.mock_Rates.reset_mock()
371
+ self.mock_Rates.return_value.returns.mean.reset_mock()
372
+ self.mock_Rates.return_value.returns.std.reset_mock()
373
+
374
+ expected_var_result = 5.7794 # Calculated manually, see thought process
375
+ # P - P * (norm.ppf(1-c, mu, sigma) + 1)
376
+ # norm.ppf(0.05, 0.0001, 0.0005) approx -0.000722425
377
+ # 8000 - 8000 * (-0.000722425 + 1) = 5.7794
378
+
379
+ # Mock var_cov_var as it's tested separately and involves scipy.stats.norm.ppf
380
+ with patch.object(
381
+ self.risk_manager, "var_cov_var", return_value=expected_var_result
382
+ ) as mock_vcv:
383
+ result = self.risk_manager.calculate_var() # Uses default tf="D1", c=0.95
384
+ self.assertAlmostEqual(result, expected_var_result, places=4)
385
+ # Check that Rates was called correctly
386
+ mock_vcv.assert_called_once()
387
+
388
+ # Test with different tf and c
389
+ # Reset mocks before the second call block
390
+ self.mock_Rates.reset_mock()
391
+ self.mock_Rates.return_value.returns.mean.reset_mock()
392
+ self.mock_Rates.return_value.returns.std.reset_mock()
393
+
394
+ custom_tf = "1h" # tf_int = 60
395
+ custom_c = 0.99
396
+ # interval = round((480/60)*252) = 2016
397
+ expected_var_custom = 10.0 # Dummy value for this call
398
+ with patch.object(
399
+ self.risk_manager, "var_cov_var", return_value=expected_var_custom
400
+ ) as mock_vcv_custom:
401
+ result_custom = self.risk_manager.calculate_var(tf=custom_tf, c=custom_c)
402
+ self.assertEqual(result_custom, expected_var_custom)
403
+
404
+ mock_vcv_custom.assert_called_once()
405
+
406
+ def test_var_cov_var(self):
407
+ # Test variance-covariance VaR calculation
408
+ result = self.risk_manager.var_cov_var(P=10000, c=0.95, mu=0.001, sigma=0.02)
409
+ self.assertGreater(result, 0)
410
+
411
+ def test_get_stop_loss(self):
412
+ min_sl_expected = 1019
413
+
414
+ # Path 1: self.sl is not None
415
+ original_sl = self.risk_manager.sl
416
+ self.risk_manager.sl = 500
417
+ self.assertEqual(
418
+ self.risk_manager.get_stop_loss(), max(500, min_sl_expected)
419
+ ) # 1020
420
+ self.risk_manager.sl = 1500
421
+ self.assertEqual(
422
+ self.risk_manager.get_stop_loss(), max(1500, min_sl_expected)
423
+ ) # 1500
424
+ self.risk_manager.sl = original_sl # Reset
425
+
426
+ # Path 2: self.sl is None and self.std is True (default from setUp)
427
+ # self.risk_manager.std is True from setUp
428
+ # self.risk_manager.sl is None from setUp
429
+ # It will call self.get_std_stop(). From test_get_std_stop, this is 1020.
430
+ with patch.object(
431
+ self.risk_manager, "get_std_stop", return_value=1020
432
+ ) as mock_get_std_stop:
433
+ self.assertEqual(
434
+ self.risk_manager.get_stop_loss(), max(1020, min_sl_expected)
435
+ ) # 1020
436
+ mock_get_std_stop.assert_called_once()
437
+
438
+ # Path 3: self.sl is None and self.std is False
439
+ original_std = self.risk_manager.std
440
+ self.risk_manager.std = False
441
+
442
+ # Subcase 3a: currency_risk returns non-zero trade_loss
443
+ mock_currency_risk_vals = {"currency_risk": 200.0, "trade_loss": 2.0}
444
+ with patch.object(
445
+ self.risk_manager, "currency_risk", return_value=mock_currency_risk_vals
446
+ ) as mock_curr_risk:
447
+ # sl_calc = round(200.0 / 2.0) = 100
448
+ self.assertEqual(
449
+ self.risk_manager.get_stop_loss(), max(100, min_sl_expected)
450
+ ) # 1020
451
+ mock_curr_risk.assert_called_once()
452
+
453
+ # Subcase 3b: currency_risk returns zero trade_loss
454
+ mock_currency_risk_vals_zero_loss = {"currency_risk": 200.0, "trade_loss": 0.0}
455
+ with patch.object(
456
+ self.risk_manager,
457
+ "currency_risk",
458
+ return_value=mock_currency_risk_vals_zero_loss,
459
+ ) as mock_curr_risk_zero:
460
+ self.assertEqual(self.risk_manager.get_stop_loss(), min_sl_expected) # 1020
461
+ mock_curr_risk_zero.assert_called_once()
462
+
463
+ self.risk_manager.std = original_std # Reset
464
+
465
+ def test_get_take_profit(self):
466
+ # Path 1: self.tp is not None
467
+ original_tp = self.risk_manager.tp
468
+ self.risk_manager.tp = 500
469
+ # expected = 500 + deviation = 500 + 1000 = 1500
470
+ self.assertEqual(self.risk_manager.get_take_profit(), 1499)
471
+ self.risk_manager.tp = original_tp # Reset
472
+
473
+ # Path 2: self.tp is None
474
+ # Calls self.get_stop_loss() * self.rr
475
+ # Let's mock get_stop_loss to isolate this test
476
+ mock_sl_value = 200
477
+ expected_tp_calc = round(
478
+ mock_sl_value * self.risk_manager.rr
479
+ ) # round(200 * 1.5) = 300
480
+ with patch.object(
481
+ self.risk_manager, "get_stop_loss", return_value=mock_sl_value
482
+ ) as mock_get_sl:
483
+ self.assertEqual(self.risk_manager.get_take_profit(), expected_tp_calc)
484
+ mock_get_sl.assert_called_once()
485
+
486
+ # Test with different rr
487
+ original_rr = self.risk_manager.rr
488
+ self.risk_manager.rr = 2.0
489
+ expected_tp_calc_new_rr = round(mock_sl_value * 2.0) # round(200 * 2.0) = 400
490
+ with patch.object(
491
+ self.risk_manager, "get_stop_loss", return_value=mock_sl_value
492
+ ) as mock_get_sl_2:
493
+ self.assertEqual(
494
+ self.risk_manager.get_take_profit(), expected_tp_calc_new_rr
495
+ )
496
+ mock_get_sl_2.assert_called_once()
497
+ self.risk_manager.rr = original_rr # Reset
498
+
499
+ def test_get_currency_risk(self):
500
+ # Allows self.risk_manager.currency_risk() to run.
501
+ # Expected 'currency_risk' key from currency_risk() with setUp mocks is var_loss_value = 5.0
502
+ expected_risk_value = 5.0
503
+
504
+ with patch.object(
505
+ self.risk_manager, "risk_level", return_value=1.0
506
+ ), patch.object(
507
+ self.risk_manager, "get_symbol_type", return_value=SymbolType.FOREX
508
+ ):
509
+ result = (
510
+ self.risk_manager.get_currency_risk()
511
+ ) # This is the method under test
512
+
513
+ self.assertEqual(result, round(expected_risk_value, 2))
514
+
515
+ def test_expected_profit(self):
516
+ # Allows self.risk_manager.get_currency_risk() (and thus currency_risk()) to run.
517
+ # self.risk_manager.rr is 1.5 from setUp.
518
+ # Expected currency_risk from get_currency_risk() is 5.0.
519
+ expected_profit_value = round(5.0 * 1.5, 2) # 7.5
520
+
521
+ with patch.object(
522
+ self.risk_manager, "risk_level", return_value=1.0
523
+ ), patch.object(
524
+ self.risk_manager, "get_symbol_type", return_value=SymbolType.FOREX
525
+ ):
526
+ result = self.risk_manager.expected_profit()
527
+
528
+ self.assertEqual(result, expected_profit_value)
529
+
530
+ def test__convert_time_frame(self):
531
+ self.assertEqual(self.risk_manager._convert_time_frame("1m"), TIMEFRAMES["1m"])
532
+ self.assertEqual(self.risk_manager._convert_time_frame("5m"), TIMEFRAMES["5m"])
533
+ self.assertEqual(self.risk_manager._convert_time_frame("1h"), 60)
534
+ self.assertEqual(self.risk_manager._convert_time_frame("4h"), 240)
535
+
536
+ # For 'D1', 'W1', 'MN1', it calls get_minutes()
537
+ # self.risk_manager.get_minutes() is tested and returns 480.0 with current setUp.
538
+ # To make this test more isolated, we can mock get_minutes here.
539
+ with patch.object(
540
+ self.risk_manager, "get_minutes", return_value=480.0
541
+ ) as mock_get_minutes:
542
+ self.assertEqual(self.risk_manager._convert_time_frame("D1"), 480.0)
543
+ mock_get_minutes.assert_called_once()
544
+ mock_get_minutes.reset_mock() # Reset for subsequent calls within this test
545
+ self.assertEqual(self.risk_manager._convert_time_frame("W1"), 480.0 * 5)
546
+ mock_get_minutes.assert_called_once()
547
+ mock_get_minutes.reset_mock()
548
+ self.assertEqual(self.risk_manager._convert_time_frame("MN1"), 480.0 * 22)
549
+ mock_get_minutes.assert_called_once()
550
+
551
+ def test__volume_step(self):
552
+ self.assertEqual(self.risk_manager._volume_step(0.01), 2)
553
+ self.assertEqual(self.risk_manager._volume_step(0.1), 1)
554
+ self.assertEqual(self.risk_manager._volume_step(1.0), 0)
555
+ self.assertEqual(self.risk_manager._volume_step(1), 0) # Test with int
556
+ self.assertEqual(self.risk_manager._volume_step(0.10001), 5)
557
+ self.assertEqual(self.risk_manager._volume_step(10), 0) # Test with int > 1
558
+
559
+ def test__check_lot(self):
560
+ # symbol_info_mock has volume_min=0.01, volume_max=100.0
561
+ self.assertEqual(self.risk_manager._check_lot(50.0), 50.0) # Within limits
562
+ self.assertEqual(self.risk_manager._check_lot(0.001), 0.01) # Below min
563
+ self.assertEqual(self.risk_manager._check_lot(0.01), 0.01) # Equal to min
564
+ self.assertEqual(self.risk_manager._check_lot(100.0), 100.0) # Equal to max
565
+ self.assertEqual(
566
+ self.risk_manager._check_lot(200.0), 50.0
567
+ ) # Above max (returns max / 2 as per code)
568
+
569
+ def test_get_trade_risk(self):
570
+ # get_trade_risk = (self.daily_dd or (self.max_risk - total_risk)) / max_trades
571
+ # or 0 if total_risk >= self.max_risk
572
+
573
+ # Scenario 1: total_risk < max_risk, daily_dd is set
574
+ # self.daily_dd = 2.0 (from setUp), self.max_risk = 5.0 (from setUp)
575
+ with patch.object(
576
+ self.risk_manager, "risk_level", return_value=1.0
577
+ ) as mock_rl, patch.object(
578
+ self.risk_manager, "max_trade", return_value=10
579
+ ) as mock_mt:
580
+ expected_trade_risk = self.risk_manager.daily_dd / 10
581
+ self.assertAlmostEqual(
582
+ self.risk_manager.get_trade_risk(), expected_trade_risk
583
+ )
584
+ mock_rl.assert_called_once()
585
+ mock_mt.assert_called_once()
586
+
587
+ # Scenario 2: total_risk < max_risk, daily_dd is None
588
+ original_daily_dd = self.risk_manager.daily_dd
589
+ self.risk_manager.daily_dd = None
590
+ with patch.object(
591
+ self.risk_manager, "risk_level", return_value=1.0
592
+ ) as mock_rl, patch.object(
593
+ self.risk_manager, "max_trade", return_value=10
594
+ ) as mock_mt:
595
+ expected_trade_risk = (self.risk_manager.max_risk - 1.0) / 10
596
+ self.assertAlmostEqual(
597
+ self.risk_manager.get_trade_risk(), expected_trade_risk
598
+ )
599
+ self.risk_manager.daily_dd = original_daily_dd # Reset
600
+
601
+ # Scenario 3: total_risk >= max_risk
602
+ with patch.object(
603
+ self.risk_manager, "risk_level", return_value=self.risk_manager.max_risk
604
+ ) as mock_rl:
605
+ self.assertEqual(self.risk_manager.get_trade_risk(), 0)
606
+ with patch.object(
607
+ self.risk_manager, "risk_level", return_value=self.risk_manager.max_risk + 1
608
+ ) as mock_rl:
609
+ self.assertEqual(self.risk_manager.get_trade_risk(), 0)
610
+
611
+ def test_get_deviation(self):
612
+ # self.symbol_info_mock.spread is 1000 from setUp
613
+ self.assertEqual(
614
+ self.risk_manager.get_deviation(), self.symbol_info_mock.spread
615
+ )
616
+
617
+ def test_get_break_even(self):
618
+ # self.symbol_info_mock.spread is 1000
619
+ original_be_attr = (
620
+ self.risk_manager.be
621
+ ) # Store original be attribute from __init__
622
+
623
+ # Path 1: self.be is an int
624
+ self.risk_manager.be = 50 # Directly set attribute for test
625
+ self.assertEqual(self.risk_manager.get_break_even(), 50)
626
+
627
+ # Path 2: self.be is a float (calls get_pchange_stop)
628
+ self.risk_manager.be = 0.5 # 0.5% pchange
629
+ with patch.object(
630
+ self.risk_manager, "get_pchange_stop", return_value=120
631
+ ) as mock_get_pchange:
632
+ self.assertEqual(self.risk_manager.get_break_even(), 120)
633
+ mock_get_pchange.assert_called_once_with(0.5)
634
+
635
+ self.risk_manager.be = None # Reset for next path
636
+
637
+ # Path 3: self.be is None (logic based on get_stop_loss and spread)
638
+ # Case 3a: stop <= 100. Example: stop = 80. be = round((80+1000)*0.5) = round(540) = 540
639
+ with patch.object(self.risk_manager, "get_stop_loss", return_value=80):
640
+ self.assertEqual(self.risk_manager.get_break_even(), 540)
641
+
642
+ # Case 3b: stop > 100 and stop <= 150. Example: stop = 120. be = round((120+1000)*0.35) = round(392) = 392
643
+ with patch.object(self.risk_manager, "get_stop_loss", return_value=120):
644
+ self.assertEqual(self.risk_manager.get_break_even(), 392)
645
+
646
+ # Case 3c: stop > 150. Example: stop = 200. be = round((200+1000)*0.25) = round(300) = 300
647
+ with patch.object(self.risk_manager, "get_stop_loss", return_value=200):
648
+ self.assertEqual(self.risk_manager.get_break_even(), 300)
649
+
650
+ self.risk_manager.be = original_be_attr # Restore original be attribute
651
+
652
+ def test_is_risk_ok(self):
653
+ # self.max_risk is 5.0 from setUp
654
+ with patch.object(self.risk_manager, "risk_level", return_value=4.0) as mock_rl:
655
+ self.assertTrue(self.risk_manager.is_risk_ok())
656
+ mock_rl.assert_called_once()
657
+
658
+ with patch.object(self.risk_manager, "risk_level", return_value=5.0) as mock_rl:
659
+ self.assertTrue(self.risk_manager.is_risk_ok()) # risk_level <= max_risk
660
+ mock_rl.assert_called_once()
661
+
662
+ with patch.object(self.risk_manager, "risk_level", return_value=5.1) as mock_rl:
663
+ self.assertFalse(self.risk_manager.is_risk_ok())
664
+ mock_rl.assert_called_once()
665
+
666
+ def test_dailydd_property(self):
667
+ # Test getter
668
+ self.assertEqual(self.risk_manager.dailydd, self.daily_risk) # From setUp
669
+ # Test setter
670
+ new_daily_risk = 3.0
671
+ self.risk_manager.dailydd = new_daily_risk
672
+ self.assertEqual(self.risk_manager.dailydd, new_daily_risk)
673
+ # Reset to original value from setUp if necessary for other tests (though instance is new per test)
674
+ self.risk_manager.dailydd = self.daily_risk
675
+
676
+ def test_maxrisk_property(self):
677
+ # Test getter
678
+ self.assertEqual(self.risk_manager.maxrisk, self.max_risk) # From setUp
679
+ # Test setter
680
+ new_max_risk = 10.0
681
+ self.risk_manager.maxrisk = new_max_risk
682
+ self.assertEqual(self.risk_manager.maxrisk, new_max_risk)
683
+ # Reset to original value
684
+ self.risk_manager.maxrisk = self.max_risk
685
+
686
+ def tearDown(self):
687
+ self.datetime_patcher.stop()
688
+ self.mt5_patcher.stop()
689
+ self.Rates_patcher.stop()
690
+ self.check_mt5_connection_patcher.stop()
691
+ self.account_get_account_info_patcher.stop()
692
+ self.account_get_symbol_info_patcher.stop()
693
+ self.account_get_trades_history_patcher.stop()
694
+ self.account_get_terminal_info_patcher.stop()
695
+ self.riskmanagement_var_cov_var_patcher.stop()
696
+ self.rm_get_leverage_patcher.stop()
697
+
698
+
699
+ if __name__ == "__main__":
700
+ unittest.main()