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.
- bbstrader/__init__.py +10 -1
- bbstrader/__main__.py +5 -0
- bbstrader/apps/_copier.py +3 -3
- bbstrader/btengine/strategy.py +113 -38
- bbstrader/metatrader/account.py +51 -26
- bbstrader/metatrader/analysis.py +30 -16
- bbstrader/metatrader/copier.py +75 -40
- bbstrader/metatrader/trade.py +29 -39
- bbstrader/metatrader/utils.py +5 -4
- bbstrader/models/nlp.py +83 -66
- bbstrader/trading/execution.py +39 -22
- bbstrader/tseries.py +103 -127
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.6.dist-info}/METADATA +7 -21
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.6.dist-info}/RECORD +31 -18
- bbstrader-0.3.6.dist-info/top_level.txt +3 -0
- docs/conf.py +56 -0
- tests/__init__.py +0 -0
- tests/engine/__init__.py +1 -0
- tests/engine/test_backtest.py +58 -0
- tests/engine/test_data.py +536 -0
- tests/engine/test_events.py +300 -0
- tests/engine/test_execution.py +219 -0
- tests/engine/test_portfolio.py +307 -0
- tests/metatrader/__init__.py +0 -0
- tests/metatrader/test_account.py +1769 -0
- tests/metatrader/test_rates.py +292 -0
- tests/metatrader/test_risk_management.py +700 -0
- tests/metatrader/test_trade.py +439 -0
- bbstrader-0.3.5.dist-info/top_level.txt +0 -1
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.6.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.6.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -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()
|