bbstrader 0.3.5__py3-none-any.whl → 0.3.7__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 +11 -2
- bbstrader/__main__.py +6 -1
- bbstrader/apps/_copier.py +43 -40
- bbstrader/btengine/backtest.py +33 -28
- bbstrader/btengine/data.py +105 -81
- bbstrader/btengine/event.py +21 -22
- bbstrader/btengine/execution.py +51 -24
- bbstrader/btengine/performance.py +23 -12
- bbstrader/btengine/portfolio.py +40 -30
- bbstrader/btengine/scripts.py +13 -12
- bbstrader/btengine/strategy.py +396 -134
- bbstrader/compat.py +4 -3
- bbstrader/config.py +20 -36
- bbstrader/core/data.py +76 -48
- bbstrader/core/scripts.py +22 -21
- bbstrader/core/utils.py +13 -12
- 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 +45 -22
- bbstrader/tseries.py +158 -166
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/METADATA +7 -21
- bbstrader-0.3.7.dist-info/RECORD +62 -0
- bbstrader-0.3.7.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 +308 -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/RECORD +0 -49
- bbstrader-0.3.5.dist-info/top_level.txt +0 -1
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.5.dist-info → bbstrader-0.3.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1769 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from io import StringIO
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from bbstrader.metatrader.account import (
|
|
10
|
+
__BROKERS__,
|
|
11
|
+
FTMO,
|
|
12
|
+
SUPPORTED_BROKERS,
|
|
13
|
+
Account,
|
|
14
|
+
AdmiralMarktsGroup,
|
|
15
|
+
Broker,
|
|
16
|
+
JustGlobalMarkets,
|
|
17
|
+
PepperstoneGroupLimited,
|
|
18
|
+
)
|
|
19
|
+
from bbstrader.metatrader.utils import (
|
|
20
|
+
AccountInfo,
|
|
21
|
+
BookInfo,
|
|
22
|
+
InvalidBroker,
|
|
23
|
+
OrderCheckResult,
|
|
24
|
+
OrderSentResult,
|
|
25
|
+
SymbolInfo,
|
|
26
|
+
SymbolType,
|
|
27
|
+
TerminalInfo,
|
|
28
|
+
TIMEFRAMES,
|
|
29
|
+
TickDtype,
|
|
30
|
+
TickFlag,
|
|
31
|
+
RateDtype,
|
|
32
|
+
RateInfo,
|
|
33
|
+
TickInfo,
|
|
34
|
+
TradeDeal,
|
|
35
|
+
TradeOrder,
|
|
36
|
+
TradePosition,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestAccount(unittest.TestCase):
|
|
41
|
+
def setUp(self):
|
|
42
|
+
# Patch the MetaTrader5 module
|
|
43
|
+
self.mt5_patcher = patch("bbstrader.metatrader.account.mt5")
|
|
44
|
+
self.mock_mt5 = self.mt5_patcher.start()
|
|
45
|
+
|
|
46
|
+
# Configure the mock mt5 object
|
|
47
|
+
# Add relevant MT5 constants for error codes
|
|
48
|
+
self.mock_mt5.RES_S_OK = 0
|
|
49
|
+
self.mock_mt5.RES_E_FAIL = 1
|
|
50
|
+
self.mock_mt5.RES_E_INVALID_PARAMS = 2
|
|
51
|
+
self.mock_mt5.RES_E_NOT_FOUND = 3
|
|
52
|
+
self.mock_mt5.RES_E_INVALID_VERSION = 4
|
|
53
|
+
self.mock_mt5.RES_E_AUTH_FAILED = 5
|
|
54
|
+
self.mock_mt5.RES_E_UNSUPPORTED = 6
|
|
55
|
+
self.mock_mt5.RES_E_AUTO_TRADING_DISABLED = 7
|
|
56
|
+
self.mock_mt5.RES_E_INTERNAL_FAIL_SEND = 8
|
|
57
|
+
self.mock_mt5.RES_E_INTERNAL_FAIL_RECEIVE = 9
|
|
58
|
+
self.mock_mt5.RES_E_INTERNAL_FAIL_INIT = 10
|
|
59
|
+
self.mock_mt5.RES_E_INTERNAL_FAIL_CONNECT = 11
|
|
60
|
+
self.mock_mt5.RES_E_INTERNAL_FAIL_TIMEOUT = 12
|
|
61
|
+
# Add trade action constants that might be used in tests
|
|
62
|
+
self.mock_mt5.TRADE_ACTION_DEAL = 1
|
|
63
|
+
self.mock_mt5.ORDER_TYPE_BUY = 0
|
|
64
|
+
self.mock_mt5.ORDER_TYPE_SELL = 1
|
|
65
|
+
self.mock_mt5.ORDER_FILLING_FOK = 1
|
|
66
|
+
self.mock_mt5.ORDER_FILLING_IOC = 2
|
|
67
|
+
self.mock_mt5.ORDER_FILLING_RETURN = 3
|
|
68
|
+
self.mock_mt5.ORDER_TIME_GTC = 0
|
|
69
|
+
self.mock_mt5.ORDER_TIME_DAY = 1
|
|
70
|
+
self.mock_mt5.ORDER_TIME_SPECIFIED = 2
|
|
71
|
+
self.mock_mt5.ORDER_TIME_SPECIFIED_DAY = 3
|
|
72
|
+
|
|
73
|
+
self.mock_mt5.initialize.return_value = True
|
|
74
|
+
self.mock_mt5.account_info.return_value = AccountInfo(
|
|
75
|
+
login=12345,
|
|
76
|
+
trade_mode=0,
|
|
77
|
+
leverage=100,
|
|
78
|
+
limit_orders=0,
|
|
79
|
+
margin_so_mode=0,
|
|
80
|
+
trade_allowed=True,
|
|
81
|
+
trade_expert=True,
|
|
82
|
+
margin_mode=0,
|
|
83
|
+
currency_digits=2,
|
|
84
|
+
fifo_close=False,
|
|
85
|
+
balance=10000.0,
|
|
86
|
+
credit=0.0,
|
|
87
|
+
profit=0.0,
|
|
88
|
+
equity=10000.0,
|
|
89
|
+
margin=0.0,
|
|
90
|
+
margin_free=10000.0,
|
|
91
|
+
margin_level=0.0,
|
|
92
|
+
margin_so_call=0.0,
|
|
93
|
+
margin_so_so=0.0,
|
|
94
|
+
margin_initial=0.0,
|
|
95
|
+
margin_maintenance=0.0,
|
|
96
|
+
assets=0.0,
|
|
97
|
+
liabilities=0.0,
|
|
98
|
+
commission_blocked=0.0,
|
|
99
|
+
name="Test Account",
|
|
100
|
+
server="Test Server",
|
|
101
|
+
currency="USD",
|
|
102
|
+
company=__BROKERS__["AMG"],
|
|
103
|
+
)
|
|
104
|
+
self.mock_mt5.terminal_info.return_value = TerminalInfo(
|
|
105
|
+
community_account=False,
|
|
106
|
+
community_connection=False,
|
|
107
|
+
connected=True,
|
|
108
|
+
dlls_allowed=True,
|
|
109
|
+
trade_allowed=True,
|
|
110
|
+
tradeapi_disabled=False,
|
|
111
|
+
email_enabled=False,
|
|
112
|
+
ftp_enabled=False,
|
|
113
|
+
notifications_enabled=False,
|
|
114
|
+
mqid=False,
|
|
115
|
+
build=1355,
|
|
116
|
+
maxbars=100000,
|
|
117
|
+
codepage=0,
|
|
118
|
+
ping_last=0,
|
|
119
|
+
community_balance=0.0,
|
|
120
|
+
retransmission=0.0,
|
|
121
|
+
company=__BROKERS__["AMG"],
|
|
122
|
+
name="MetaTrader 5",
|
|
123
|
+
language="en",
|
|
124
|
+
path="",
|
|
125
|
+
data_path="",
|
|
126
|
+
commondata_path="",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Instantiate the Account class
|
|
130
|
+
self.account = Account()
|
|
131
|
+
|
|
132
|
+
def tearDown(self):
|
|
133
|
+
# Stop the patcher
|
|
134
|
+
self.mt5_patcher.stop()
|
|
135
|
+
|
|
136
|
+
def test_get_account_info_default(self):
|
|
137
|
+
# Test get_account_info with no arguments
|
|
138
|
+
account_info = self.account.get_account_info()
|
|
139
|
+
self.assertIsNotNone(account_info)
|
|
140
|
+
self.assertEqual(account_info.login, 12345)
|
|
141
|
+
self.assertEqual(account_info.name, "Test Account")
|
|
142
|
+
|
|
143
|
+
def test_get_account_info_with_credentials(self):
|
|
144
|
+
# Test get_account_info with account, password, and server
|
|
145
|
+
mock_specific_account_info = AccountInfo(
|
|
146
|
+
login=67890,
|
|
147
|
+
trade_mode=0,
|
|
148
|
+
leverage=50,
|
|
149
|
+
limit_orders=0,
|
|
150
|
+
margin_so_mode=0,
|
|
151
|
+
trade_allowed=True,
|
|
152
|
+
trade_expert=True,
|
|
153
|
+
margin_mode=0,
|
|
154
|
+
currency_digits=2,
|
|
155
|
+
fifo_close=False,
|
|
156
|
+
balance=5000.0,
|
|
157
|
+
credit=0.0,
|
|
158
|
+
profit=0.0,
|
|
159
|
+
equity=5000.0,
|
|
160
|
+
margin=0.0,
|
|
161
|
+
margin_free=5000.0,
|
|
162
|
+
margin_level=0.0,
|
|
163
|
+
margin_so_call=0.0,
|
|
164
|
+
margin_so_so=0.0,
|
|
165
|
+
margin_initial=0.0,
|
|
166
|
+
margin_maintenance=0.0,
|
|
167
|
+
assets=0.0,
|
|
168
|
+
liabilities=0.0,
|
|
169
|
+
commission_blocked=0.0,
|
|
170
|
+
name="Specific Account",
|
|
171
|
+
server="Specific Server",
|
|
172
|
+
currency="EUR",
|
|
173
|
+
company="Specific Company",
|
|
174
|
+
)
|
|
175
|
+
self.mock_mt5.login.return_value = True
|
|
176
|
+
|
|
177
|
+
# Set the return_value directly for the mt5.account_info() call
|
|
178
|
+
# that will occur within self.account.get_account_info() for this specific test.
|
|
179
|
+
original_account_info_mock = (
|
|
180
|
+
self.mock_mt5.account_info.return_value
|
|
181
|
+
) # Preserve from setUp # noqa: F841
|
|
182
|
+
self.mock_mt5.account_info.return_value = mock_specific_account_info
|
|
183
|
+
|
|
184
|
+
account_info_returned = self.account.get_account_info(
|
|
185
|
+
account=67890, password="password", server="Specific Server"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Restore mock for other tests if this wasn't the last action (though setUp handles isolation)
|
|
189
|
+
self.mock_mt5.account_info.return_value = (
|
|
190
|
+
original_account_info_mock # Not strictly needed due to test isolation
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self.mock_mt5.login.assert_called_once_with(
|
|
194
|
+
67890, password="password", server="Specific Server", timeout=60000
|
|
195
|
+
)
|
|
196
|
+
self.assertIsNotNone(account_info_returned)
|
|
197
|
+
self.assertEqual(account_info_returned.login, 67890)
|
|
198
|
+
self.assertEqual(account_info_returned.name, "Specific Account")
|
|
199
|
+
self.assertEqual(account_info_returned.currency, "EUR")
|
|
200
|
+
|
|
201
|
+
def test_get_account_info_login_fails(self):
|
|
202
|
+
self.mock_mt5.login.return_value = False
|
|
203
|
+
# RES_E_AUTH_FAILED is a common code for login failures.
|
|
204
|
+
self.mock_mt5.last_error.return_value = (
|
|
205
|
+
self.mock_mt5.RES_E_AUTH_FAILED,
|
|
206
|
+
"Login authorization failed",
|
|
207
|
+
)
|
|
208
|
+
with self.assertRaises(Exception):
|
|
209
|
+
self.account.get_account_info(
|
|
210
|
+
account=67890, password="password", server="Specific Server"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def test_get_account_info_returns_none(self):
|
|
214
|
+
self.mock_mt5.account_info.return_value = None
|
|
215
|
+
# Reset side_effect if it was set in another test
|
|
216
|
+
self.mock_mt5.account_info.side_effect = None
|
|
217
|
+
self.mock_mt5.last_error.return_value = (2, "Account info not found")
|
|
218
|
+
|
|
219
|
+
# Call get_account_info without credentials first to reset internal state if necessary
|
|
220
|
+
ret_val = self.account.get_account_info()
|
|
221
|
+
self.assertIsNone(ret_val)
|
|
222
|
+
|
|
223
|
+
@patch("bbstrader.metatrader.account.print") # Patched print in the account module
|
|
224
|
+
def test_show_account_info_success(
|
|
225
|
+
self, mock_print
|
|
226
|
+
): # mock_print is now the mock for the print function
|
|
227
|
+
# Reset side_effect for account_info to ensure it returns the default mock value
|
|
228
|
+
self.mock_mt5.account_info.side_effect = None
|
|
229
|
+
# Ensure a valid AccountInfo object is returned by the mock
|
|
230
|
+
self.mock_mt5.account_info.return_value = AccountInfo(
|
|
231
|
+
login=12345,
|
|
232
|
+
trade_mode=0,
|
|
233
|
+
leverage=100,
|
|
234
|
+
limit_orders=0,
|
|
235
|
+
margin_so_mode=0,
|
|
236
|
+
trade_allowed=True,
|
|
237
|
+
trade_expert=True,
|
|
238
|
+
margin_mode=0,
|
|
239
|
+
currency_digits=2,
|
|
240
|
+
fifo_close=False,
|
|
241
|
+
balance=10000.0,
|
|
242
|
+
credit=0.0,
|
|
243
|
+
profit=0.0,
|
|
244
|
+
equity=10000.0,
|
|
245
|
+
margin=0.0,
|
|
246
|
+
margin_free=10000.0,
|
|
247
|
+
margin_level=0.0,
|
|
248
|
+
margin_so_call=0.0,
|
|
249
|
+
margin_so_so=0.0,
|
|
250
|
+
margin_initial=0.0,
|
|
251
|
+
margin_maintenance=0.0,
|
|
252
|
+
assets=0.0,
|
|
253
|
+
liabilities=0.0,
|
|
254
|
+
commission_blocked=0.0,
|
|
255
|
+
name="Test Account",
|
|
256
|
+
server="Test Server",
|
|
257
|
+
currency="USD",
|
|
258
|
+
company=__BROKERS__["AMG"],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
self.account.show_account_info()
|
|
262
|
+
|
|
263
|
+
# Check that print was called with the header
|
|
264
|
+
# The f-string in source is print(f"
|
|
265
|
+
header_found = False
|
|
266
|
+
dataframe_output_found = False
|
|
267
|
+
for call_args in mock_print.call_args_list:
|
|
268
|
+
args, _ = call_args
|
|
269
|
+
if args: # Ensure there are positional arguments
|
|
270
|
+
printed_text = str(args[0])
|
|
271
|
+
if "ACCOUNT INFORMATIONS:" in printed_text:
|
|
272
|
+
header_found = True
|
|
273
|
+
if (
|
|
274
|
+
"Test Account" in printed_text
|
|
275
|
+
and "12345" in printed_text
|
|
276
|
+
and "PROPERTY" in printed_text
|
|
277
|
+
): # Assuming PROPERTY is a column name in df.to_string()
|
|
278
|
+
dataframe_output_found = True
|
|
279
|
+
|
|
280
|
+
self.assertTrue(header_found, "Header 'ACCOUNT INFORMATIONS:' not printed.")
|
|
281
|
+
self.assertTrue(
|
|
282
|
+
dataframe_output_found,
|
|
283
|
+
"DataFrame content (Test Account, 12345, PROPERTY) not found in print calls.",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@patch("sys.stdout", new_callable=StringIO)
|
|
287
|
+
def test_show_account_info_failure(
|
|
288
|
+
self, mock_stdout
|
|
289
|
+
): # Added mock_stdout from original
|
|
290
|
+
self.mock_mt5.account_info.return_value = None
|
|
291
|
+
self.mock_mt5.account_info.side_effect = None # Clear side effect
|
|
292
|
+
self.mock_mt5.last_error.return_value = (
|
|
293
|
+
self.mock_mt5.RES_E_NOT_FOUND,
|
|
294
|
+
"Account info not found",
|
|
295
|
+
)
|
|
296
|
+
with self.assertRaises(Exception):
|
|
297
|
+
self.account.show_account_info()
|
|
298
|
+
|
|
299
|
+
def test_get_terminal_info_success(self):
|
|
300
|
+
terminal_info = self.account.get_terminal_info()
|
|
301
|
+
self.assertIsNotNone(terminal_info)
|
|
302
|
+
# Assert against the company name explicitly set in setUp's mock TerminalInfo
|
|
303
|
+
self.assertEqual(terminal_info.company, __BROKERS__["AMG"])
|
|
304
|
+
self.assertEqual(terminal_info.name, "MetaTrader 5")
|
|
305
|
+
|
|
306
|
+
def test_get_terminal_info_failure(self):
|
|
307
|
+
self.mock_mt5.terminal_info.return_value = None
|
|
308
|
+
self.mock_mt5.last_error.return_value = (3, "Terminal info not found")
|
|
309
|
+
ret_val = self.account.get_terminal_info()
|
|
310
|
+
self.assertIsNone(ret_val)
|
|
311
|
+
|
|
312
|
+
@patch("bbstrader.metatrader.account.print") # Patched print in the account module
|
|
313
|
+
def test_get_terminal_info_show_success(
|
|
314
|
+
self, mock_print
|
|
315
|
+
): # mock_print for the print function
|
|
316
|
+
# Ensure terminal_info mock is set up (done in setUp)
|
|
317
|
+
# self.mock_mt5.terminal_info.return_value is already set in setUp.
|
|
318
|
+
|
|
319
|
+
self.account.get_terminal_info(show=True)
|
|
320
|
+
|
|
321
|
+
# Check that print was called with the DataFrame's string representation
|
|
322
|
+
# This output will contain column names like "PROPERTY" and values.
|
|
323
|
+
dataframe_output_found = False
|
|
324
|
+
for call_args in mock_print.call_args_list:
|
|
325
|
+
args, _ = call_args
|
|
326
|
+
if args:
|
|
327
|
+
printed_text = str(args[0])
|
|
328
|
+
if (
|
|
329
|
+
"PROPERTY" in printed_text
|
|
330
|
+
and __BROKERS__["AMG"] in printed_text
|
|
331
|
+
and "MetaTrader 5" in printed_text
|
|
332
|
+
):
|
|
333
|
+
dataframe_output_found = True
|
|
334
|
+
|
|
335
|
+
self.assertTrue(
|
|
336
|
+
dataframe_output_found,
|
|
337
|
+
"DataFrame content (PROPERTY, broker company, terminal name) not found in print calls.",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
@patch("bbstrader.metatrader.account.urllib.request.urlretrieve")
|
|
341
|
+
@patch("bbstrader.metatrader.account.CurrencyConverter")
|
|
342
|
+
@patch("bbstrader.metatrader.account.os.path.isfile")
|
|
343
|
+
@patch("bbstrader.metatrader.account.os.remove")
|
|
344
|
+
def test_convert_currencies(
|
|
345
|
+
self,
|
|
346
|
+
mock_os_remove,
|
|
347
|
+
mock_os_path_isfile,
|
|
348
|
+
mock_currency_converter,
|
|
349
|
+
mock_urlretrieve,
|
|
350
|
+
):
|
|
351
|
+
# Mock that the file doesn't exist so it tries to download
|
|
352
|
+
mock_os_path_isfile.return_value = False
|
|
353
|
+
|
|
354
|
+
# Mock the CurrencyConverter
|
|
355
|
+
mock_converter_instance = MagicMock()
|
|
356
|
+
mock_converter_instance.currencies = {"USD", "EUR", "JPY"}
|
|
357
|
+
mock_converter_instance.convert.return_value = 110.0 # Example conversion rate
|
|
358
|
+
mock_currency_converter.return_value = mock_converter_instance
|
|
359
|
+
|
|
360
|
+
# Test conversion
|
|
361
|
+
result = self.account.convert_currencies(100, "USD", "JPY")
|
|
362
|
+
self.assertEqual(result, 110.0)
|
|
363
|
+
mock_urlretrieve.assert_called_once() # Ensure download was attempted
|
|
364
|
+
mock_currency_converter.assert_called_once()
|
|
365
|
+
mock_converter_instance.convert.assert_called_once_with(
|
|
366
|
+
amount=100, currency="USD", new_currency="JPY"
|
|
367
|
+
)
|
|
368
|
+
mock_os_remove.assert_called_once() # Ensure cleanup was attempted
|
|
369
|
+
|
|
370
|
+
@patch("bbstrader.metatrader.account.urllib.request.urlretrieve")
|
|
371
|
+
@patch("bbstrader.metatrader.account.CurrencyConverter")
|
|
372
|
+
@patch("bbstrader.metatrader.account.os.path.isfile")
|
|
373
|
+
@patch("bbstrader.metatrader.account.os.remove")
|
|
374
|
+
def test_convert_currencies_unsupported(
|
|
375
|
+
self,
|
|
376
|
+
mock_os_remove,
|
|
377
|
+
mock_os_path_isfile,
|
|
378
|
+
mock_currency_converter,
|
|
379
|
+
mock_urlretrieve,
|
|
380
|
+
):
|
|
381
|
+
# To avoid UnboundLocalError for 'c' with current source code, ensure 'c' is defined.
|
|
382
|
+
# This means the block 'if not os.path.isfile(filename):' must be entered.
|
|
383
|
+
mock_os_path_isfile.return_value = False # Changed from True
|
|
384
|
+
|
|
385
|
+
mock_converter_instance = MagicMock()
|
|
386
|
+
mock_converter_instance.currencies = {
|
|
387
|
+
"USD",
|
|
388
|
+
"EUR",
|
|
389
|
+
} # JPY is not supported for conversion
|
|
390
|
+
mock_currency_converter.return_value = mock_converter_instance
|
|
391
|
+
|
|
392
|
+
result = self.account.convert_currencies(100, "USD", "JPY")
|
|
393
|
+
self.assertEqual(
|
|
394
|
+
result, 100
|
|
395
|
+
) # Should return original amount due to unsupported target currency
|
|
396
|
+
|
|
397
|
+
# Assertions adjusted for the new path
|
|
398
|
+
mock_urlretrieve.assert_called_once() # Download is now attempted
|
|
399
|
+
mock_currency_converter.assert_called_once() # CurrencyConverter is initialized
|
|
400
|
+
# os.remove is called in the source code after 'c = CurrencyConverter(filename)'
|
|
401
|
+
# and before 'supported = c.currencies'
|
|
402
|
+
mock_os_remove.assert_called_once()
|
|
403
|
+
|
|
404
|
+
def test_get_currency_rates(self):
|
|
405
|
+
mock_symbol_info = SymbolInfo(
|
|
406
|
+
custom=False,
|
|
407
|
+
chart_mode=0,
|
|
408
|
+
select=True,
|
|
409
|
+
visible=True,
|
|
410
|
+
session_deals=0,
|
|
411
|
+
session_buy_orders=0,
|
|
412
|
+
session_sell_orders=0,
|
|
413
|
+
volume=0,
|
|
414
|
+
volumehigh=0,
|
|
415
|
+
volumelow=0,
|
|
416
|
+
time=datetime.now(),
|
|
417
|
+
digits=5,
|
|
418
|
+
spread=0,
|
|
419
|
+
spread_float=True,
|
|
420
|
+
ticks_bookdepth=0,
|
|
421
|
+
trade_calc_mode=0,
|
|
422
|
+
trade_mode=0,
|
|
423
|
+
start_time=0,
|
|
424
|
+
expiration_time=0,
|
|
425
|
+
trade_stops_level=0,
|
|
426
|
+
trade_freeze_level=0,
|
|
427
|
+
trade_exemode=0,
|
|
428
|
+
swap_mode=0,
|
|
429
|
+
swap_rollover3days=0,
|
|
430
|
+
margin_hedged_use_leg=False,
|
|
431
|
+
expiration_mode=0,
|
|
432
|
+
filling_mode=0,
|
|
433
|
+
order_mode=0,
|
|
434
|
+
order_gtc_mode=0,
|
|
435
|
+
option_mode=0,
|
|
436
|
+
option_right=0,
|
|
437
|
+
bid=1.0,
|
|
438
|
+
bidhigh=1.0,
|
|
439
|
+
bidlow=1.0,
|
|
440
|
+
ask=1.0,
|
|
441
|
+
askhigh=1.0,
|
|
442
|
+
asklow=1.0,
|
|
443
|
+
last=1.0,
|
|
444
|
+
lasthigh=1.0,
|
|
445
|
+
lastlow=1.0,
|
|
446
|
+
volume_real=0,
|
|
447
|
+
volumehigh_real=0,
|
|
448
|
+
volumelow_real=0,
|
|
449
|
+
option_strike=0,
|
|
450
|
+
point=0.00001,
|
|
451
|
+
trade_tick_value=0,
|
|
452
|
+
trade_tick_value_profit=0,
|
|
453
|
+
trade_tick_value_loss=0,
|
|
454
|
+
trade_tick_size=0,
|
|
455
|
+
trade_contract_size=100000,
|
|
456
|
+
trade_accrued_interest=0,
|
|
457
|
+
trade_face_value=0,
|
|
458
|
+
trade_liquidity_rate=0,
|
|
459
|
+
volume_min=0.01,
|
|
460
|
+
volume_max=100,
|
|
461
|
+
volume_step=0.01,
|
|
462
|
+
volume_limit=0,
|
|
463
|
+
swap_long=0,
|
|
464
|
+
swap_short=0,
|
|
465
|
+
margin_initial=0,
|
|
466
|
+
margin_maintenance=0,
|
|
467
|
+
session_volume=0,
|
|
468
|
+
session_turnover=0,
|
|
469
|
+
session_interest=0,
|
|
470
|
+
session_buy_orders_volume=0,
|
|
471
|
+
session_sell_orders_volume=0,
|
|
472
|
+
session_open=0,
|
|
473
|
+
session_close=0,
|
|
474
|
+
session_aw=0,
|
|
475
|
+
session_price_settlement=0,
|
|
476
|
+
session_price_limit_min=0,
|
|
477
|
+
session_price_limit_max=0,
|
|
478
|
+
margin_hedged=0,
|
|
479
|
+
price_change=0,
|
|
480
|
+
price_volatility=0,
|
|
481
|
+
price_theoretical=0,
|
|
482
|
+
price_greeks_delta=0,
|
|
483
|
+
price_greeks_theta=0,
|
|
484
|
+
price_greeks_gamma=0,
|
|
485
|
+
price_greeks_vega=0,
|
|
486
|
+
price_greeks_rho=0,
|
|
487
|
+
price_greeks_omega=0,
|
|
488
|
+
price_sensitivity=0,
|
|
489
|
+
basis="",
|
|
490
|
+
category="",
|
|
491
|
+
currency_base="EUR",
|
|
492
|
+
currency_profit="USD",
|
|
493
|
+
currency_margin="EUR",
|
|
494
|
+
bank="",
|
|
495
|
+
description="Euro vs US Dollar",
|
|
496
|
+
exchange="",
|
|
497
|
+
formula="",
|
|
498
|
+
isin="",
|
|
499
|
+
name="EURUSD",
|
|
500
|
+
page="",
|
|
501
|
+
path="Forex\\Majors\\EURUSD",
|
|
502
|
+
)
|
|
503
|
+
self.mock_mt5.symbol_info.return_value = mock_symbol_info
|
|
504
|
+
# Default account currency is USD from setUp
|
|
505
|
+
rates = self.account.get_currency_rates("EURUSD")
|
|
506
|
+
expected_rates = {"bc": "EUR", "mc": "EUR", "pc": "USD", "ac": "USD"}
|
|
507
|
+
self.assertEqual(rates, expected_rates)
|
|
508
|
+
self.mock_mt5.symbol_info.assert_called_once_with("EURUSD")
|
|
509
|
+
|
|
510
|
+
def _get_mock_symbol_info(
|
|
511
|
+
self,
|
|
512
|
+
name="EURUSD",
|
|
513
|
+
path="Forex\\Majors\\EURUSD",
|
|
514
|
+
description="Euro vs US Dollar",
|
|
515
|
+
currency_base="EUR",
|
|
516
|
+
currency_profit="USD",
|
|
517
|
+
currency_margin="EUR",
|
|
518
|
+
time_val=1678886400,
|
|
519
|
+
):
|
|
520
|
+
return SymbolInfo(
|
|
521
|
+
custom=False,
|
|
522
|
+
chart_mode=0,
|
|
523
|
+
select=True,
|
|
524
|
+
visible=True,
|
|
525
|
+
session_deals=0,
|
|
526
|
+
session_buy_orders=0,
|
|
527
|
+
session_sell_orders=0,
|
|
528
|
+
volume=0,
|
|
529
|
+
volumehigh=0,
|
|
530
|
+
volumelow=0,
|
|
531
|
+
time=datetime.fromtimestamp(time_val),
|
|
532
|
+
digits=5,
|
|
533
|
+
spread=0,
|
|
534
|
+
spread_float=True,
|
|
535
|
+
ticks_bookdepth=0,
|
|
536
|
+
trade_calc_mode=0,
|
|
537
|
+
trade_mode=0,
|
|
538
|
+
start_time=0,
|
|
539
|
+
expiration_time=0,
|
|
540
|
+
trade_stops_level=0,
|
|
541
|
+
trade_freeze_level=0,
|
|
542
|
+
trade_exemode=0,
|
|
543
|
+
swap_mode=0,
|
|
544
|
+
swap_rollover3days=0,
|
|
545
|
+
margin_hedged_use_leg=False,
|
|
546
|
+
expiration_mode=0,
|
|
547
|
+
filling_mode=0,
|
|
548
|
+
order_mode=0,
|
|
549
|
+
order_gtc_mode=0,
|
|
550
|
+
option_mode=0,
|
|
551
|
+
option_right=0,
|
|
552
|
+
bid=1.0,
|
|
553
|
+
bidhigh=1.0,
|
|
554
|
+
bidlow=1.0,
|
|
555
|
+
ask=1.0,
|
|
556
|
+
askhigh=1.0,
|
|
557
|
+
asklow=1.0,
|
|
558
|
+
last=1.0,
|
|
559
|
+
lasthigh=1.0,
|
|
560
|
+
lastlow=1.0,
|
|
561
|
+
volume_real=0,
|
|
562
|
+
volumehigh_real=0,
|
|
563
|
+
volumelow_real=0,
|
|
564
|
+
option_strike=0,
|
|
565
|
+
point=0.00001,
|
|
566
|
+
trade_tick_value=0,
|
|
567
|
+
trade_tick_value_profit=0,
|
|
568
|
+
trade_tick_value_loss=0,
|
|
569
|
+
trade_tick_size=0,
|
|
570
|
+
trade_contract_size=100000,
|
|
571
|
+
trade_accrued_interest=0,
|
|
572
|
+
trade_face_value=0,
|
|
573
|
+
trade_liquidity_rate=0,
|
|
574
|
+
volume_min=0.01,
|
|
575
|
+
volume_max=100,
|
|
576
|
+
volume_step=0.01,
|
|
577
|
+
volume_limit=0,
|
|
578
|
+
swap_long=0,
|
|
579
|
+
swap_short=0,
|
|
580
|
+
margin_initial=0,
|
|
581
|
+
margin_maintenance=0,
|
|
582
|
+
session_volume=0,
|
|
583
|
+
session_turnover=0,
|
|
584
|
+
session_interest=0,
|
|
585
|
+
session_buy_orders_volume=0,
|
|
586
|
+
session_sell_orders_volume=0,
|
|
587
|
+
session_open=0,
|
|
588
|
+
session_close=0,
|
|
589
|
+
session_aw=0,
|
|
590
|
+
session_price_settlement=0,
|
|
591
|
+
session_price_limit_min=0,
|
|
592
|
+
session_price_limit_max=0,
|
|
593
|
+
margin_hedged=0,
|
|
594
|
+
price_change=0,
|
|
595
|
+
price_volatility=0,
|
|
596
|
+
price_theoretical=0,
|
|
597
|
+
price_greeks_delta=0,
|
|
598
|
+
price_greeks_theta=0,
|
|
599
|
+
price_greeks_gamma=0,
|
|
600
|
+
price_greeks_vega=0,
|
|
601
|
+
price_greeks_rho=0,
|
|
602
|
+
price_greeks_omega=0,
|
|
603
|
+
price_sensitivity=0,
|
|
604
|
+
basis="",
|
|
605
|
+
category="",
|
|
606
|
+
currency_base=currency_base,
|
|
607
|
+
currency_profit=currency_profit,
|
|
608
|
+
currency_margin=currency_margin,
|
|
609
|
+
bank="",
|
|
610
|
+
description=description,
|
|
611
|
+
exchange="",
|
|
612
|
+
formula="",
|
|
613
|
+
isin="",
|
|
614
|
+
name=name,
|
|
615
|
+
page="",
|
|
616
|
+
path=path,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
def test_get_symbols_all(self):
|
|
620
|
+
mock_symbols_data = []
|
|
621
|
+
for symbol_name in ["EURUSD", "AAPL", "[ES]"]:
|
|
622
|
+
mock = MagicMock()
|
|
623
|
+
mock.name = symbol_name
|
|
624
|
+
mock_symbols_data.append(mock)
|
|
625
|
+
|
|
626
|
+
self.mock_mt5.symbols_get.return_value = mock_symbols_data
|
|
627
|
+
self.mock_mt5.symbol_info.side_effect = [
|
|
628
|
+
self._get_mock_symbol_info(name="EURUSD", path="Forex\\Majors\\EURUSD"),
|
|
629
|
+
self._get_mock_symbol_info(
|
|
630
|
+
name="AAPL", path="Stocks\\US\\AAPL", description="Apple Inc."
|
|
631
|
+
),
|
|
632
|
+
self._get_mock_symbol_info(
|
|
633
|
+
name="[ES]", path="Futures\\Indices\\ES", description="E-mini S&P 500"
|
|
634
|
+
),
|
|
635
|
+
]
|
|
636
|
+
|
|
637
|
+
symbols = self.account.get_symbols(symbol_type="ALL")
|
|
638
|
+
self.assertEqual(len(symbols), 3)
|
|
639
|
+
self.assertIn("EURUSD", symbols)
|
|
640
|
+
self.assertIn("AAPL", symbols)
|
|
641
|
+
self.assertIn("[ES]", symbols)
|
|
642
|
+
self.mock_mt5.symbols_get.assert_called_once()
|
|
643
|
+
|
|
644
|
+
def test_get_symbols_filtered_forex(self):
|
|
645
|
+
mock_symbols_data = []
|
|
646
|
+
for symbol_name in ["EURUSD", "USDJPY", "USDJPY"]:
|
|
647
|
+
mock = MagicMock()
|
|
648
|
+
mock.name = symbol_name
|
|
649
|
+
mock_symbols_data.append(mock)
|
|
650
|
+
|
|
651
|
+
self.mock_mt5.symbols_get.return_value = mock_symbols_data
|
|
652
|
+
self.mock_mt5.symbol_info.side_effect = [
|
|
653
|
+
self._get_mock_symbol_info(name="EURUSD", path="Forex\\Majors\\EURUSD"),
|
|
654
|
+
self._get_mock_symbol_info(name="USDJPY", path="Forex\\Majors\\USDJPY"),
|
|
655
|
+
self._get_mock_symbol_info(name="AAPL", path="Stocks\\US\\AAPL"),
|
|
656
|
+
]
|
|
657
|
+
symbols = self.account.get_symbols(symbol_type=SymbolType.FOREX)
|
|
658
|
+
self.assertEqual(len(symbols), 2)
|
|
659
|
+
self.assertIn("EURUSD", symbols)
|
|
660
|
+
self.assertIn("USDJPY", symbols)
|
|
661
|
+
self.assertNotIn("AAPL", symbols)
|
|
662
|
+
|
|
663
|
+
def test_get_symbols_filtered_etf_check_description(self):
|
|
664
|
+
mock_symbols_data = []
|
|
665
|
+
for symbol_name in ["SPY", "GLD"]:
|
|
666
|
+
mock = MagicMock()
|
|
667
|
+
mock.name = symbol_name
|
|
668
|
+
mock_symbols_data.append(mock)
|
|
669
|
+
|
|
670
|
+
self.mock_mt5.symbols_get.return_value = mock_symbols_data
|
|
671
|
+
self.mock_mt5.symbol_info.side_effect = [
|
|
672
|
+
self._get_mock_symbol_info(
|
|
673
|
+
name="SPY", path="ETFs\\US\\SPY", description="SPDR S&P 500 ETF Trust"
|
|
674
|
+
),
|
|
675
|
+
self._get_mock_symbol_info(
|
|
676
|
+
name="GLD", path="ETFs\\US\\GLD", description="SPDR Gold Shares"
|
|
677
|
+
), # This one should fail the check
|
|
678
|
+
]
|
|
679
|
+
with self.assertRaises(ValueError) as context:
|
|
680
|
+
self.account.get_symbols(symbol_type=SymbolType.ETFs, check_etf=True)
|
|
681
|
+
self.assertIn("doesn't have 'ETF' in its description", str(context.exception))
|
|
682
|
+
|
|
683
|
+
def test_get_symbols_save_to_file(self):
|
|
684
|
+
mock_symbols_data = [MagicMock(name="EURUSD")]
|
|
685
|
+
self.mock_mt5.symbols_get.return_value = mock_symbols_data
|
|
686
|
+
self.mock_mt5.symbol_info.return_value = self._get_mock_symbol_info(
|
|
687
|
+
name="EURUSD"
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
with patch("builtins.open", unittest.mock.mock_open()) as mock_file:
|
|
691
|
+
self.account.get_symbols(
|
|
692
|
+
save=True, file_name="test_symbols", include_desc=True
|
|
693
|
+
)
|
|
694
|
+
mock_file.assert_called_once_with(
|
|
695
|
+
"test_symbols.txt", mode="w", encoding="utf-8"
|
|
696
|
+
)
|
|
697
|
+
# Check if content was written (simplified check)
|
|
698
|
+
handle = mock_file()
|
|
699
|
+
handle.write.assert_any_call(
|
|
700
|
+
"EURUSD|Euro vs US Dollar\n"
|
|
701
|
+
) # Max length dependent
|
|
702
|
+
|
|
703
|
+
def test_get_symbols_no_symbols_found(self):
|
|
704
|
+
self.mock_mt5.symbols_get.return_value = []
|
|
705
|
+
self.mock_mt5.last_error.return_value = (
|
|
706
|
+
self.mock_mt5.RES_E_NOT_FOUND,
|
|
707
|
+
"No symbols available",
|
|
708
|
+
)
|
|
709
|
+
with self.assertRaises(Exception):
|
|
710
|
+
self.account.get_symbols()
|
|
711
|
+
|
|
712
|
+
def test_get_symbol_type(self):
|
|
713
|
+
self.mock_mt5.symbol_info.return_value = self._get_mock_symbol_info(
|
|
714
|
+
path="Forex\\Majors\\EURUSD"
|
|
715
|
+
)
|
|
716
|
+
self.assertEqual(self.account.get_symbol_type("EURUSD"), SymbolType.FOREX)
|
|
717
|
+
|
|
718
|
+
self.mock_mt5.symbol_info.return_value = self._get_mock_symbol_info(
|
|
719
|
+
path="Stocks\\US\\AAPL"
|
|
720
|
+
)
|
|
721
|
+
self.assertEqual(self.account.get_symbol_type("AAPL"), SymbolType.STOCKS)
|
|
722
|
+
|
|
723
|
+
self.mock_mt5.symbol_info.return_value = self._get_mock_symbol_info(
|
|
724
|
+
path="Futures\\Energies\\CL"
|
|
725
|
+
)
|
|
726
|
+
self.assertEqual(self.account.get_symbol_type("CL"), SymbolType.FUTURES)
|
|
727
|
+
|
|
728
|
+
def test_get_fx_symbols_unsupported_broker_raises_invalidbroker_on_init(
|
|
729
|
+
self,
|
|
730
|
+
): # Renamed for clarity
|
|
731
|
+
# Store original mock configurations to restore if needed, though setUp handles isolation
|
|
732
|
+
original_terminal_company = self.mock_mt5.terminal_info.return_value.company
|
|
733
|
+
original_account_company = self.mock_mt5.account_info.return_value.company
|
|
734
|
+
|
|
735
|
+
unsupported_broker_name = "SomeOtherBroker"
|
|
736
|
+
|
|
737
|
+
# Configure mocks to simulate an unsupported broker
|
|
738
|
+
# Both terminal_info().company and account_info().company might be checked by Account or Broker class
|
|
739
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
740
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
741
|
+
company=unsupported_broker_name
|
|
742
|
+
)
|
|
743
|
+
)
|
|
744
|
+
self.mock_mt5.account_info.return_value = (
|
|
745
|
+
self.mock_mt5.account_info.return_value._replace(
|
|
746
|
+
company=unsupported_broker_name
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
with self.assertRaises(InvalidBroker) as context:
|
|
751
|
+
# Instantiating Account with an unsupported broker (and default copy=False, backtest=False)
|
|
752
|
+
# should raise InvalidBroker.
|
|
753
|
+
Account()
|
|
754
|
+
|
|
755
|
+
self.assertIn(
|
|
756
|
+
f"{unsupported_broker_name} is not currently supported broker",
|
|
757
|
+
str(context.exception),
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# Restore original mock configurations if these mocks are used by other tests in a specific sequence
|
|
761
|
+
# (Not strictly necessary due to test isolation by setUp/tearDown for instance mocks,
|
|
762
|
+
# but good practice if class-level mocks or shared state were involved)
|
|
763
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
764
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
765
|
+
company=original_terminal_company
|
|
766
|
+
)
|
|
767
|
+
)
|
|
768
|
+
self.mock_mt5.account_info.return_value = (
|
|
769
|
+
self.mock_mt5.account_info.return_value._replace(
|
|
770
|
+
company=original_account_company
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
def test_get_future_symbols_default_broker_metals(self):
|
|
775
|
+
# AdmiralMarkets specific logic for futures categories
|
|
776
|
+
mock_symbols_data = []
|
|
777
|
+
for symbol_name in ["_XAUUSD", "_OILUSD", "COCOA", "#USTBond"]:
|
|
778
|
+
mock = MagicMock()
|
|
779
|
+
mock.name = symbol_name
|
|
780
|
+
mock_symbols_data.append(mock)
|
|
781
|
+
|
|
782
|
+
commodities_symbols_data = []
|
|
783
|
+
for symbol_name in ["XAUUSD", "OILUSD", "COCOA"]:
|
|
784
|
+
mock = MagicMock()
|
|
785
|
+
mock.name = symbol_name
|
|
786
|
+
commodities_symbols_data.append(mock)
|
|
787
|
+
|
|
788
|
+
# This setup is a bit complex due to nested calls to get_symbols and get_symbol_info
|
|
789
|
+
def symbol_info_side_effect_futures(symbol_name):
|
|
790
|
+
if symbol_name == "_XAUUSD":
|
|
791
|
+
return self._get_mock_symbol_info(
|
|
792
|
+
name="_XAUUSD", path="Futures\\Metals\\_XAUUSD"
|
|
793
|
+
)
|
|
794
|
+
if symbol_name == "_OILUSD":
|
|
795
|
+
return self._get_mock_symbol_info(
|
|
796
|
+
name="_OILUSD", path="Futures\\Energies\\_OILUSD"
|
|
797
|
+
)
|
|
798
|
+
if symbol_name == "COCOA":
|
|
799
|
+
return self._get_mock_symbol_info(
|
|
800
|
+
name="COCOA", path="Commodities\\Agricultures\\COCOA"
|
|
801
|
+
) # For the commodity check
|
|
802
|
+
if symbol_name == "XAUUSD":
|
|
803
|
+
return self._get_mock_symbol_info(
|
|
804
|
+
name="XAUUSD", path="Commodities\\Metals\\XAUUSD"
|
|
805
|
+
)
|
|
806
|
+
if symbol_name == "OILUSD":
|
|
807
|
+
return self._get_mock_symbol_info(
|
|
808
|
+
name="OILUSD", path="Commodities\\Energies\\OILUSD"
|
|
809
|
+
)
|
|
810
|
+
if symbol_name == "#USTBond":
|
|
811
|
+
return self._get_mock_symbol_info(
|
|
812
|
+
name="#USTBond", path="Futures\\Bonds\\#USTBond"
|
|
813
|
+
)
|
|
814
|
+
return self._get_mock_symbol_info(name=symbol_name)
|
|
815
|
+
|
|
816
|
+
self.mock_mt5.symbols_get.side_effect = [
|
|
817
|
+
commodities_symbols_data, # First call from get_symbols(SymbolType.COMMODITIES)
|
|
818
|
+
mock_symbols_data, # Second call from get_symbols(SymbolType.FUTURES)
|
|
819
|
+
]
|
|
820
|
+
self.mock_mt5.symbol_info.side_effect = symbol_info_side_effect_futures
|
|
821
|
+
|
|
822
|
+
symbols = self.account.get_future_symbols(category="metals")
|
|
823
|
+
self.assertIn("_XAUUSD", symbols)
|
|
824
|
+
self.assertNotIn("_OILUSD", symbols)
|
|
825
|
+
|
|
826
|
+
def test_get_symbol_info_success(self):
|
|
827
|
+
mock_info = self._get_mock_symbol_info(name="EURUSD", time_val=1678886400)
|
|
828
|
+
self.mock_mt5.symbol_info.return_value = mock_info
|
|
829
|
+
info = self.account.get_symbol_info("EURUSD")
|
|
830
|
+
self.assertIsNotNone(info)
|
|
831
|
+
self.assertEqual(info.name, "EURUSD")
|
|
832
|
+
self.assertEqual(info.time, datetime.fromtimestamp(1678886400))
|
|
833
|
+
self.mock_mt5.symbol_info.assert_called_once_with("EURUSD")
|
|
834
|
+
|
|
835
|
+
def test_get_symbol_info_not_found(self):
|
|
836
|
+
self.mock_mt5.symbol_info.return_value = None
|
|
837
|
+
# RES_E_NOT_FOUND for symbol not found
|
|
838
|
+
self.mock_mt5.last_error.return_value = (
|
|
839
|
+
self.mock_mt5.RES_E_NOT_FOUND,
|
|
840
|
+
"Symbol not found in Market Watch",
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
ret_val = self.account.get_symbol_info("UNKNOWN")
|
|
844
|
+
self.assertIsNone(ret_val) # This part should still be true
|
|
845
|
+
|
|
846
|
+
# Test that show_symbol_info raises correctly
|
|
847
|
+
with self.assertRaises(
|
|
848
|
+
Exception
|
|
849
|
+
) as context: # Expecting specific HistoryNotFound
|
|
850
|
+
self.account.show_symbol_info("UNKNOWN")
|
|
851
|
+
self.assertTrue(
|
|
852
|
+
"No history found for UNKNOWN" in str(context.exception)
|
|
853
|
+
or "Symbol not found" in str(context.exception)
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
@patch("sys.stdout", new_callable=StringIO)
|
|
857
|
+
def test_show_symbol_info_success(self, mock_stdout):
|
|
858
|
+
mock_info = self._get_mock_symbol_info(
|
|
859
|
+
name="GBPUSD", description="Great Britain Pound vs US Dollar"
|
|
860
|
+
)
|
|
861
|
+
self.mock_mt5.symbol_info.return_value = mock_info
|
|
862
|
+
self.account.show_symbol_info("GBPUSD")
|
|
863
|
+
output = mock_stdout.getvalue()
|
|
864
|
+
self.assertIn(
|
|
865
|
+
"SYMBOL INFO FOR GBPUSD (Great Britain Pound vs US Dollar)", output
|
|
866
|
+
)
|
|
867
|
+
self.assertIn("currency_base", output)
|
|
868
|
+
self.assertIn("GBP", output)
|
|
869
|
+
|
|
870
|
+
def test_get_tick_info_success(self):
|
|
871
|
+
mock_tick = TickInfo(
|
|
872
|
+
time=datetime.fromtimestamp(1678886500),
|
|
873
|
+
bid=1.05,
|
|
874
|
+
ask=1.06,
|
|
875
|
+
last=1.055,
|
|
876
|
+
volume=10,
|
|
877
|
+
time_msc=1678886500000,
|
|
878
|
+
flags=0,
|
|
879
|
+
volume_real=10.0,
|
|
880
|
+
)
|
|
881
|
+
self.mock_mt5.symbol_info_tick.return_value = mock_tick
|
|
882
|
+
tick = self.account.get_tick_info("EURUSD")
|
|
883
|
+
self.assertIsNotNone(tick)
|
|
884
|
+
self.assertEqual(tick.bid, 1.05)
|
|
885
|
+
self.assertEqual(tick.time, datetime.fromtimestamp(1678886500))
|
|
886
|
+
self.mock_mt5.symbol_info_tick.assert_called_once_with("EURUSD")
|
|
887
|
+
|
|
888
|
+
def test_get_tick_info_not_found(self):
|
|
889
|
+
self.mock_mt5.symbol_info_tick.return_value = None
|
|
890
|
+
self.mock_mt5.last_error.return_value = (6, "Tick not found")
|
|
891
|
+
ret_val = self.account.get_tick_info("UNKNOWN_TICK")
|
|
892
|
+
self.assertIsNone(ret_val)
|
|
893
|
+
|
|
894
|
+
@patch("sys.stdout", new_callable=StringIO)
|
|
895
|
+
def test_show_tick_info_success(self, mock_stdout):
|
|
896
|
+
mock_tick = TickInfo(
|
|
897
|
+
time=datetime.fromtimestamp(1678886500),
|
|
898
|
+
bid=1.05,
|
|
899
|
+
ask=1.06,
|
|
900
|
+
last=1.055,
|
|
901
|
+
volume=10,
|
|
902
|
+
time_msc=1678886500000,
|
|
903
|
+
flags=0,
|
|
904
|
+
volume_real=10.0,
|
|
905
|
+
)
|
|
906
|
+
self.mock_mt5.symbol_info_tick.return_value = mock_tick
|
|
907
|
+
# Also need to mock symbol_info for the description part in _show_info
|
|
908
|
+
self.mock_mt5.symbol_info.return_value = self._get_mock_symbol_info(
|
|
909
|
+
name="EURUSD", description="Euro vs US Dollar"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
self.account.show_tick_info("EURUSD")
|
|
913
|
+
output = mock_stdout.getvalue()
|
|
914
|
+
self.assertIn(
|
|
915
|
+
"TICK INFO FOR EURUSD", output
|
|
916
|
+
) # Description might or might not be there based on how _show_info handles TickInfo
|
|
917
|
+
self.assertIn("bid", output)
|
|
918
|
+
self.assertIn("1.05", output)
|
|
919
|
+
|
|
920
|
+
def test_get_market_book_success(self):
|
|
921
|
+
mock_book_data = (
|
|
922
|
+
BookInfo(type=0, price=1.1, volume=10.0, volume_dbl=10.0), # TYPE_BUY
|
|
923
|
+
BookInfo(type=1, price=1.2, volume=5.0, volume_dbl=5.0), # TYPE_SELL
|
|
924
|
+
)
|
|
925
|
+
self.mock_mt5.market_book_get.return_value = mock_book_data
|
|
926
|
+
book = self.account.get_market_book("EURUSD")
|
|
927
|
+
self.assertIsNotNone(book)
|
|
928
|
+
self.assertEqual(len(book), 2)
|
|
929
|
+
self.assertEqual(book[0].price, 1.1)
|
|
930
|
+
self.assertEqual(book[1].volume, 5.0)
|
|
931
|
+
self.mock_mt5.market_book_get.assert_called_once_with("EURUSD")
|
|
932
|
+
|
|
933
|
+
def test_get_market_book_empty(self):
|
|
934
|
+
self.mock_mt5.market_book_get.return_value = None
|
|
935
|
+
self.mock_mt5.last_error.return_value = (
|
|
936
|
+
7,
|
|
937
|
+
"Market book empty",
|
|
938
|
+
) # Example error
|
|
939
|
+
book = self.account.get_market_book("EMPTYBOOK")
|
|
940
|
+
self.assertIsNone(book)
|
|
941
|
+
|
|
942
|
+
def test_calculate_margin_success(self):
|
|
943
|
+
self.mock_mt5.order_calc_margin.return_value = 150.75
|
|
944
|
+
margin = self.account.calculate_margin(
|
|
945
|
+
action="buy", symbol="EURUSD", lot=0.1, price=1.1000
|
|
946
|
+
)
|
|
947
|
+
self.assertEqual(margin, 150.75)
|
|
948
|
+
self.mock_mt5.order_calc_margin.assert_called_once_with(
|
|
949
|
+
self.mock_mt5.ORDER_TYPE_BUY, "EURUSD", 0.1, 1.1000
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
def test_calculate_profit_success(self):
|
|
953
|
+
self.mock_mt5.order_calc_profit.return_value = 150.75
|
|
954
|
+
margin = self.account.calculate_profit("buy", "EURUSD", 0.1, 1.1000, 1.2000)
|
|
955
|
+
self.assertEqual(margin, 150.75)
|
|
956
|
+
self.mock_mt5.order_calc_profit.assert_called_once_with(
|
|
957
|
+
self.mock_mt5.ORDER_TYPE_BUY, "EURUSD", 0.1, 1.1000, 1.2000
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
def test_calculate_margin_error(self):
|
|
961
|
+
self.mock_mt5.order_calc_margin.side_effect = Exception("Calculation error")
|
|
962
|
+
self.mock_mt5.last_error.return_value = (
|
|
963
|
+
self.mock_mt5.RES_E_FAIL,
|
|
964
|
+
"Calc error detail",
|
|
965
|
+
) # 1 is often generic MT5.RES_E_FAIL
|
|
966
|
+
with self.assertRaises(Exception) as context:
|
|
967
|
+
self.account.calculate_margin(
|
|
968
|
+
action="sell", symbol="GBPUSD", lot=0.5, price=1.2500
|
|
969
|
+
)
|
|
970
|
+
self.assertTrue(
|
|
971
|
+
"Calc error detail" in str(context.exception)
|
|
972
|
+
or "Calculation error" in str(context.exception)
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
def test_calculate_profit_error(self):
|
|
976
|
+
self.mock_mt5.order_calc_profit.side_effect = Exception("Calculation error")
|
|
977
|
+
self.mock_mt5.last_error.return_value = (
|
|
978
|
+
self.mock_mt5.RES_E_FAIL,
|
|
979
|
+
"Calc error detail",
|
|
980
|
+
) # 1 is often generic MT5.RES_E_FAIL
|
|
981
|
+
with self.assertRaises(Exception) as context:
|
|
982
|
+
self.account.calculate_profit("sell", "GBPUSD", 0.5, 1.2500, 1.3500)
|
|
983
|
+
self.assertTrue(
|
|
984
|
+
"Calc error detail" in str(context.exception)
|
|
985
|
+
or "Calculation error" in str(context.exception)
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
def test_check_order_success(self):
|
|
989
|
+
# Mock the TradeRequest object that would be part of OrderCheckResult.request
|
|
990
|
+
# This should simulate the MqlTradeRequest structure returned by MT5
|
|
991
|
+
mock_mql_trade_request = MagicMock()
|
|
992
|
+
mock_mql_trade_request.action = self.mock_mt5.TRADE_ACTION_DEAL
|
|
993
|
+
mock_mql_trade_request.symbol = "EURUSD"
|
|
994
|
+
mock_mql_trade_request.volume = 0.1
|
|
995
|
+
mock_mql_trade_request.price = 1.1
|
|
996
|
+
mock_mql_trade_request.type = self.mock_mt5.ORDER_TYPE_BUY
|
|
997
|
+
mock_mql_trade_request.magic = 123
|
|
998
|
+
mock_mql_trade_request.order = 0
|
|
999
|
+
mock_mql_trade_request.stoplimit = 0.0
|
|
1000
|
+
mock_mql_trade_request.sl = 0.0
|
|
1001
|
+
mock_mql_trade_request.tp = 0.0
|
|
1002
|
+
mock_mql_trade_request.deviation = 0
|
|
1003
|
+
mock_mql_trade_request.type_filling = self.mock_mt5.ORDER_FILLING_FOK
|
|
1004
|
+
mock_mql_trade_request.type_time = self.mock_mt5.ORDER_TIME_GTC
|
|
1005
|
+
mock_mql_trade_request.expiration = 0
|
|
1006
|
+
mock_mql_trade_request.comment = "test check"
|
|
1007
|
+
mock_mql_trade_request.position = 0
|
|
1008
|
+
mock_mql_trade_request.position_by = 0
|
|
1009
|
+
|
|
1010
|
+
# The _asdict() method is crucial for named tuples
|
|
1011
|
+
mock_mql_trade_request._asdict.return_value = {
|
|
1012
|
+
"action": mock_mql_trade_request.action,
|
|
1013
|
+
"symbol": mock_mql_trade_request.symbol,
|
|
1014
|
+
"volume": mock_mql_trade_request.volume,
|
|
1015
|
+
"price": mock_mql_trade_request.price,
|
|
1016
|
+
"type": mock_mql_trade_request.type,
|
|
1017
|
+
"magic": mock_mql_trade_request.magic,
|
|
1018
|
+
"order": mock_mql_trade_request.order,
|
|
1019
|
+
"stoplimit": mock_mql_trade_request.stoplimit,
|
|
1020
|
+
"sl": mock_mql_trade_request.sl,
|
|
1021
|
+
"tp": mock_mql_trade_request.tp,
|
|
1022
|
+
"deviation": mock_mql_trade_request.deviation,
|
|
1023
|
+
"type_filling": mock_mql_trade_request.type_filling,
|
|
1024
|
+
"type_time": mock_mql_trade_request.type_time,
|
|
1025
|
+
"expiration": mock_mql_trade_request.expiration,
|
|
1026
|
+
"comment": mock_mql_trade_request.comment,
|
|
1027
|
+
"position": mock_mql_trade_request.position,
|
|
1028
|
+
"position_by": mock_mql_trade_request.position_by,
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
mock_result = MagicMock(spec=OrderCheckResult) # Use spec for better mocking
|
|
1032
|
+
mock_result.retcode = 0
|
|
1033
|
+
mock_result.balance = 10000.0
|
|
1034
|
+
mock_result.equity = 10000.0
|
|
1035
|
+
mock_result.profit = 0.0
|
|
1036
|
+
mock_result.margin = 50.0
|
|
1037
|
+
mock_result.margin_free = 9950.0
|
|
1038
|
+
mock_result.margin_level = 20000.0
|
|
1039
|
+
mock_result.comment = "Order check OK"
|
|
1040
|
+
mock_result.request = mock_mql_trade_request # Assign the detailed mock here
|
|
1041
|
+
|
|
1042
|
+
# The _asdict() for the main OrderCheckResult object
|
|
1043
|
+
mock_result._asdict.return_value = {
|
|
1044
|
+
"retcode": mock_result.retcode,
|
|
1045
|
+
"balance": mock_result.balance,
|
|
1046
|
+
"equity": mock_result.equity,
|
|
1047
|
+
"profit": mock_result.profit,
|
|
1048
|
+
"margin": mock_result.margin,
|
|
1049
|
+
"margin_free": mock_result.margin_free,
|
|
1050
|
+
"margin_level": mock_result.margin_level,
|
|
1051
|
+
"comment": mock_result.comment,
|
|
1052
|
+
"request": mock_mql_trade_request,
|
|
1053
|
+
}
|
|
1054
|
+
self.mock_mt5.order_check.return_value = mock_result
|
|
1055
|
+
|
|
1056
|
+
# Reconstruct the request to pass to the method
|
|
1057
|
+
check_request = {
|
|
1058
|
+
"action": self.mock_mt5.TRADE_ACTION_DEAL,
|
|
1059
|
+
"symbol": "EURUSD",
|
|
1060
|
+
"volume": 0.1,
|
|
1061
|
+
"price": 1.1,
|
|
1062
|
+
"type": self.mock_mt5.ORDER_TYPE_BUY,
|
|
1063
|
+
"magic": 123,
|
|
1064
|
+
"order": 0,
|
|
1065
|
+
"stoplimit": 0.0,
|
|
1066
|
+
"sl": 0.0,
|
|
1067
|
+
"tp": 0.0,
|
|
1068
|
+
"deviation": 0,
|
|
1069
|
+
"type_filling": self.mock_mt5.ORDER_FILLING_FOK,
|
|
1070
|
+
"type_time": self.mock_mt5.ORDER_TIME_GTC,
|
|
1071
|
+
"expiration": 0,
|
|
1072
|
+
"comment": "test check",
|
|
1073
|
+
"position": 0,
|
|
1074
|
+
"position_by": 0,
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
result = self.account.check_order(check_request)
|
|
1078
|
+
self.assertIsNotNone(result)
|
|
1079
|
+
self.assertEqual(result.retcode, 0)
|
|
1080
|
+
self.assertEqual(result.comment, "Order check OK")
|
|
1081
|
+
self.mock_mt5.order_check.assert_called_once_with(check_request)
|
|
1082
|
+
self.assertEqual(
|
|
1083
|
+
result.request.symbol, "EURUSD"
|
|
1084
|
+
) # Accessing the TradeRequest object
|
|
1085
|
+
|
|
1086
|
+
def test_send_order_success(self):
|
|
1087
|
+
mock_mql_trade_request = MagicMock()
|
|
1088
|
+
mock_mql_trade_request.action = self.mock_mt5.TRADE_ACTION_DEAL
|
|
1089
|
+
mock_mql_trade_request.symbol = "EURUSD"
|
|
1090
|
+
mock_mql_trade_request.volume = 0.1
|
|
1091
|
+
mock_mql_trade_request.price = 1.1
|
|
1092
|
+
mock_mql_trade_request.type = self.mock_mt5.ORDER_TYPE_BUY
|
|
1093
|
+
mock_mql_trade_request.magic = 123
|
|
1094
|
+
mock_mql_trade_request.order = 0 # For new orders, order ticket is 0
|
|
1095
|
+
mock_mql_trade_request.stoplimit = 0.0
|
|
1096
|
+
mock_mql_trade_request.sl = 0.0
|
|
1097
|
+
mock_mql_trade_request.tp = 0.0
|
|
1098
|
+
mock_mql_trade_request.deviation = 10 # Example deviation
|
|
1099
|
+
mock_mql_trade_request.type_filling = self.mock_mt5.ORDER_FILLING_FOK
|
|
1100
|
+
mock_mql_trade_request.type_time = self.mock_mt5.ORDER_TIME_GTC
|
|
1101
|
+
mock_mql_trade_request.expiration = 0
|
|
1102
|
+
mock_mql_trade_request.comment = "test send"
|
|
1103
|
+
mock_mql_trade_request.position = 0
|
|
1104
|
+
mock_mql_trade_request.position_by = 0
|
|
1105
|
+
|
|
1106
|
+
mock_mql_trade_request._asdict.return_value = {
|
|
1107
|
+
"action": mock_mql_trade_request.action,
|
|
1108
|
+
"symbol": mock_mql_trade_request.symbol,
|
|
1109
|
+
"volume": mock_mql_trade_request.volume,
|
|
1110
|
+
"price": mock_mql_trade_request.price,
|
|
1111
|
+
"type": mock_mql_trade_request.type,
|
|
1112
|
+
"magic": mock_mql_trade_request.magic,
|
|
1113
|
+
"order": mock_mql_trade_request.order,
|
|
1114
|
+
"stoplimit": mock_mql_trade_request.stoplimit,
|
|
1115
|
+
"sl": mock_mql_trade_request.sl,
|
|
1116
|
+
"tp": mock_mql_trade_request.tp,
|
|
1117
|
+
"deviation": mock_mql_trade_request.deviation,
|
|
1118
|
+
"type_filling": mock_mql_trade_request.type_filling,
|
|
1119
|
+
"type_time": mock_mql_trade_request.type_time,
|
|
1120
|
+
"expiration": mock_mql_trade_request.expiration,
|
|
1121
|
+
"comment": mock_mql_trade_request.comment,
|
|
1122
|
+
"position": mock_mql_trade_request.position,
|
|
1123
|
+
"position_by": mock_mql_trade_request.position_by,
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
mock_result = MagicMock(spec=OrderSentResult)
|
|
1127
|
+
mock_result.retcode = 10009 # Request completed
|
|
1128
|
+
mock_result.deal = 12345
|
|
1129
|
+
mock_result.order = 54321 # Actual order ticket assigned by server
|
|
1130
|
+
mock_result.volume = 0.1
|
|
1131
|
+
mock_result.price = 1.1
|
|
1132
|
+
mock_result.bid = 1.0990
|
|
1133
|
+
mock_result.ask = 1.1010
|
|
1134
|
+
mock_result.comment = "Request completed"
|
|
1135
|
+
mock_result.request_id = 0
|
|
1136
|
+
mock_result.retcode_external = 0
|
|
1137
|
+
mock_result.request = mock_mql_trade_request
|
|
1138
|
+
|
|
1139
|
+
mock_result._asdict.return_value = {
|
|
1140
|
+
"retcode": mock_result.retcode,
|
|
1141
|
+
"deal": mock_result.deal,
|
|
1142
|
+
"order": mock_result.order,
|
|
1143
|
+
"volume": mock_result.volume,
|
|
1144
|
+
"price": mock_result.price,
|
|
1145
|
+
"bid": mock_result.bid,
|
|
1146
|
+
"ask": mock_result.ask,
|
|
1147
|
+
"comment": mock_result.comment,
|
|
1148
|
+
"request_id": mock_result.request_id,
|
|
1149
|
+
"retcode_external": mock_result.retcode_external,
|
|
1150
|
+
"request": mock_mql_trade_request,
|
|
1151
|
+
}
|
|
1152
|
+
self.mock_mt5.order_send.return_value = mock_result
|
|
1153
|
+
|
|
1154
|
+
send_request = {
|
|
1155
|
+
"action": self.mock_mt5.TRADE_ACTION_DEAL,
|
|
1156
|
+
"symbol": "EURUSD",
|
|
1157
|
+
"volume": 0.1,
|
|
1158
|
+
"price": 1.1,
|
|
1159
|
+
"type": self.mock_mt5.ORDER_TYPE_BUY,
|
|
1160
|
+
"magic": 123,
|
|
1161
|
+
"order": 0,
|
|
1162
|
+
"stoplimit": 0.0,
|
|
1163
|
+
"sl": 0.0,
|
|
1164
|
+
"tp": 0.0,
|
|
1165
|
+
"deviation": 10,
|
|
1166
|
+
"type_filling": self.mock_mt5.ORDER_FILLING_FOK,
|
|
1167
|
+
"type_time": self.mock_mt5.ORDER_TIME_GTC,
|
|
1168
|
+
"expiration": 0,
|
|
1169
|
+
"comment": "test send",
|
|
1170
|
+
"position": 0,
|
|
1171
|
+
"position_by": 0,
|
|
1172
|
+
}
|
|
1173
|
+
result = self.account.send_order(send_request)
|
|
1174
|
+
self.assertIsNotNone(result)
|
|
1175
|
+
self.assertEqual(result.retcode, 10009)
|
|
1176
|
+
self.assertEqual(result.order, 54321)
|
|
1177
|
+
self.mock_mt5.order_send.assert_called_once_with(send_request)
|
|
1178
|
+
self.assertEqual(
|
|
1179
|
+
result.request.symbol, "EURUSD"
|
|
1180
|
+
) # Accessing the TradeRequest object
|
|
1181
|
+
|
|
1182
|
+
def _get_mock_position(
|
|
1183
|
+
self, ticket=1, symbol="EURUSD", volume=0.1, price_open=1.1, type_val=0
|
|
1184
|
+
): # type_val=0 for buy
|
|
1185
|
+
return TradePosition(
|
|
1186
|
+
ticket=ticket,
|
|
1187
|
+
time=int(datetime.now().timestamp()),
|
|
1188
|
+
time_msc=0,
|
|
1189
|
+
time_update=0,
|
|
1190
|
+
time_update_msc=0,
|
|
1191
|
+
type=type_val,
|
|
1192
|
+
magic=0,
|
|
1193
|
+
identifier=0,
|
|
1194
|
+
reason=0,
|
|
1195
|
+
volume=volume,
|
|
1196
|
+
price_open=price_open,
|
|
1197
|
+
sl=0,
|
|
1198
|
+
tp=0,
|
|
1199
|
+
price_current=price_open + 0.001,
|
|
1200
|
+
swap=0,
|
|
1201
|
+
profit=10.0,
|
|
1202
|
+
symbol=symbol,
|
|
1203
|
+
comment="test",
|
|
1204
|
+
external_id="",
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
def test_get_positions_all_as_tuple(self):
|
|
1208
|
+
mock_positions_data = [
|
|
1209
|
+
self._get_mock_position(ticket=101, symbol="EURUSD"),
|
|
1210
|
+
self._get_mock_position(
|
|
1211
|
+
ticket=102, symbol="GBPUSD", type_val=1
|
|
1212
|
+
), # type_val=1 for sell
|
|
1213
|
+
]
|
|
1214
|
+
self.mock_mt5.positions_get.return_value = mock_positions_data
|
|
1215
|
+
positions = self.account.get_positions(to_df=False)
|
|
1216
|
+
self.assertIsNotNone(positions)
|
|
1217
|
+
self.assertIsInstance(positions, tuple)
|
|
1218
|
+
self.assertEqual(len(positions), 2)
|
|
1219
|
+
self.assertEqual(positions[0].ticket, 101)
|
|
1220
|
+
self.assertEqual(positions[1].symbol, "GBPUSD")
|
|
1221
|
+
self.mock_mt5.positions_get.assert_called_once_with()
|
|
1222
|
+
|
|
1223
|
+
def test_get_positions_by_symbol_as_df(self):
|
|
1224
|
+
mock_positions_data = [self._get_mock_position(symbol="AAPL")]
|
|
1225
|
+
self.mock_mt5.positions_get.return_value = mock_positions_data
|
|
1226
|
+
positions_df = self.account.get_positions(symbol="AAPL", to_df=True)
|
|
1227
|
+
self.assertIsNotNone(positions_df)
|
|
1228
|
+
self.assertIsInstance(positions_df, pd.DataFrame)
|
|
1229
|
+
self.assertEqual(len(positions_df), 1)
|
|
1230
|
+
self.assertEqual(positions_df.iloc[0]["symbol"], "AAPL")
|
|
1231
|
+
self.mock_mt5.positions_get.assert_called_once_with(symbol="AAPL")
|
|
1232
|
+
|
|
1233
|
+
def test_get_positions_no_positions(self):
|
|
1234
|
+
self.mock_mt5.positions_get.return_value = [] # Empty list
|
|
1235
|
+
positions = self.account.get_positions()
|
|
1236
|
+
self.assertIsNone(positions)
|
|
1237
|
+
|
|
1238
|
+
self.mock_mt5.positions_get.return_value = None # None
|
|
1239
|
+
positions = self.account.get_positions()
|
|
1240
|
+
self.assertIsNone(positions)
|
|
1241
|
+
|
|
1242
|
+
def _get_mock_deal(
|
|
1243
|
+
self,
|
|
1244
|
+
ticket=201,
|
|
1245
|
+
order=54321,
|
|
1246
|
+
symbol="EURUSD",
|
|
1247
|
+
volume=0.1,
|
|
1248
|
+
price=1.1,
|
|
1249
|
+
type_val=0,
|
|
1250
|
+
entry=0,
|
|
1251
|
+
time=None,
|
|
1252
|
+
position_id=101,
|
|
1253
|
+
):
|
|
1254
|
+
deal_time = int(time if time is not None else datetime.now().timestamp())
|
|
1255
|
+
return TradeDeal(
|
|
1256
|
+
ticket=ticket,
|
|
1257
|
+
order=order,
|
|
1258
|
+
time=deal_time,
|
|
1259
|
+
time_msc=deal_time * 1000, # Match time_msc with time
|
|
1260
|
+
type=type_val,
|
|
1261
|
+
entry=entry,
|
|
1262
|
+
magic=0,
|
|
1263
|
+
position_id=position_id,
|
|
1264
|
+
reason=0,
|
|
1265
|
+
volume=volume,
|
|
1266
|
+
price=price,
|
|
1267
|
+
commission=0,
|
|
1268
|
+
swap=0,
|
|
1269
|
+
profit=10.0,
|
|
1270
|
+
fee=0,
|
|
1271
|
+
symbol=symbol,
|
|
1272
|
+
comment="test deal",
|
|
1273
|
+
external_id="",
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
def test_get_trades_history_date_range_as_df(self):
|
|
1277
|
+
mock_deals_data = [
|
|
1278
|
+
self._get_mock_deal(
|
|
1279
|
+
ticket=201, symbol="EURUSD", time=int(datetime(2023, 1, 15).timestamp())
|
|
1280
|
+
),
|
|
1281
|
+
self._get_mock_deal(
|
|
1282
|
+
ticket=202, symbol="GBPUSD", time=int(datetime(2023, 1, 16).timestamp())
|
|
1283
|
+
),
|
|
1284
|
+
]
|
|
1285
|
+
self.mock_mt5.history_deals_get.return_value = mock_deals_data
|
|
1286
|
+
from_date = datetime(2023, 1, 1)
|
|
1287
|
+
to_date = datetime(2023, 1, 31)
|
|
1288
|
+
history_df = self.account.get_trades_history(
|
|
1289
|
+
date_from=from_date, date_to=to_date, to_df=True
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
self.assertIsNotNone(history_df)
|
|
1293
|
+
self.assertIsInstance(history_df, pd.DataFrame)
|
|
1294
|
+
self.assertEqual(len(history_df), 2)
|
|
1295
|
+
self.assertEqual(history_df.iloc[0]["symbol"], "EURUSD")
|
|
1296
|
+
self.mock_mt5.history_deals_get.assert_called_once_with(from_date, to_date)
|
|
1297
|
+
|
|
1298
|
+
def test_get_trades_history_by_ticket_as_tuple(self):
|
|
1299
|
+
mock_deals_data = [self._get_mock_deal(ticket=205, order=50001)]
|
|
1300
|
+
self.mock_mt5.history_deals_get.return_value = mock_deals_data
|
|
1301
|
+
history_tuple = self.account.get_trades_history(
|
|
1302
|
+
ticket=50001, to_df=False
|
|
1303
|
+
) # Filter by order ticket
|
|
1304
|
+
|
|
1305
|
+
self.assertIsNotNone(history_tuple)
|
|
1306
|
+
self.assertIsInstance(history_tuple, tuple)
|
|
1307
|
+
self.assertEqual(len(history_tuple), 1)
|
|
1308
|
+
self.assertEqual(history_tuple[0].ticket, 205)
|
|
1309
|
+
self.mock_mt5.history_deals_get.assert_called_once_with(ticket=50001)
|
|
1310
|
+
|
|
1311
|
+
def test_get_trades_history_no_deals(self):
|
|
1312
|
+
self.mock_mt5.history_deals_get.return_value = []
|
|
1313
|
+
history = self.account.get_trades_history()
|
|
1314
|
+
self.assertIsNone(history)
|
|
1315
|
+
|
|
1316
|
+
self.mock_mt5.history_deals_get.return_value = None
|
|
1317
|
+
history = self.account.get_trades_history()
|
|
1318
|
+
self.assertIsNone(history)
|
|
1319
|
+
|
|
1320
|
+
def _get_mock_order(
|
|
1321
|
+
self,
|
|
1322
|
+
ticket=301,
|
|
1323
|
+
symbol="EURUSD",
|
|
1324
|
+
price_open=1.1,
|
|
1325
|
+
volume_initial=0.1,
|
|
1326
|
+
type_val=0,
|
|
1327
|
+
time_setup=None,
|
|
1328
|
+
position_id=0,
|
|
1329
|
+
):
|
|
1330
|
+
order_time_setup = int(
|
|
1331
|
+
time_setup if time_setup is not None else datetime.now().timestamp()
|
|
1332
|
+
)
|
|
1333
|
+
return TradeOrder(
|
|
1334
|
+
ticket=ticket,
|
|
1335
|
+
time_setup=order_time_setup,
|
|
1336
|
+
time_setup_msc=order_time_setup * 1000, # Match time_setup_msc
|
|
1337
|
+
time_done=0,
|
|
1338
|
+
time_done_msc=0,
|
|
1339
|
+
time_expiration=0,
|
|
1340
|
+
type=type_val,
|
|
1341
|
+
type_time=0,
|
|
1342
|
+
type_filling=0,
|
|
1343
|
+
state=0,
|
|
1344
|
+
magic=0,
|
|
1345
|
+
position_id=position_id,
|
|
1346
|
+
position_by_id=0,
|
|
1347
|
+
reason=0,
|
|
1348
|
+
volume_initial=volume_initial,
|
|
1349
|
+
volume_current=volume_initial,
|
|
1350
|
+
price_open=price_open,
|
|
1351
|
+
sl=0,
|
|
1352
|
+
tp=0,
|
|
1353
|
+
price_current=price_open,
|
|
1354
|
+
price_stoplimit=0,
|
|
1355
|
+
symbol=symbol,
|
|
1356
|
+
comment="test order",
|
|
1357
|
+
external_id="",
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
def test_get_orders_all_as_tuple(self):
|
|
1361
|
+
mock_orders_data = [
|
|
1362
|
+
self._get_mock_order(ticket=301, symbol="EURUSD"),
|
|
1363
|
+
self._get_mock_order(
|
|
1364
|
+
ticket=302, symbol="GBPUSD", type_val=1
|
|
1365
|
+
), # type=1 SELL
|
|
1366
|
+
]
|
|
1367
|
+
self.mock_mt5.orders_get.return_value = mock_orders_data
|
|
1368
|
+
orders = self.account.get_orders(to_df=False)
|
|
1369
|
+
self.assertIsNotNone(orders)
|
|
1370
|
+
self.assertIsInstance(orders, tuple)
|
|
1371
|
+
self.assertEqual(len(orders), 2)
|
|
1372
|
+
self.assertEqual(orders[0].ticket, 301)
|
|
1373
|
+
self.mock_mt5.orders_get.assert_called_once_with()
|
|
1374
|
+
|
|
1375
|
+
def test_get_orders_by_symbol_as_df(self):
|
|
1376
|
+
mock_orders_data = [self._get_mock_order(symbol="AAPL")]
|
|
1377
|
+
self.mock_mt5.orders_get.return_value = mock_orders_data
|
|
1378
|
+
orders_df = self.account.get_orders(symbol="AAPL", to_df=True)
|
|
1379
|
+
self.assertIsNotNone(orders_df)
|
|
1380
|
+
self.assertIsInstance(orders_df, pd.DataFrame)
|
|
1381
|
+
self.assertEqual(len(orders_df), 1)
|
|
1382
|
+
self.assertEqual(orders_df.iloc[0]["symbol"], "AAPL")
|
|
1383
|
+
self.mock_mt5.orders_get.assert_called_once_with(symbol="AAPL")
|
|
1384
|
+
|
|
1385
|
+
def test_get_orders_no_orders(self):
|
|
1386
|
+
self.mock_mt5.orders_get.return_value = []
|
|
1387
|
+
orders = self.account.get_orders()
|
|
1388
|
+
self.assertIsNone(orders)
|
|
1389
|
+
|
|
1390
|
+
self.mock_mt5.orders_get.return_value = None
|
|
1391
|
+
orders = self.account.get_orders()
|
|
1392
|
+
self.assertIsNone(orders)
|
|
1393
|
+
|
|
1394
|
+
def test_get_orders_history_date_range_as_df(self):
|
|
1395
|
+
mock_orders_hist_data = [
|
|
1396
|
+
self._get_mock_order(
|
|
1397
|
+
ticket=401,
|
|
1398
|
+
symbol="XAUUSD",
|
|
1399
|
+
time_setup=int(datetime(2023, 2, 10).timestamp()),
|
|
1400
|
+
),
|
|
1401
|
+
self._get_mock_order(
|
|
1402
|
+
ticket=402,
|
|
1403
|
+
symbol="USOIL",
|
|
1404
|
+
time_setup=int(datetime(2023, 2, 12).timestamp()),
|
|
1405
|
+
),
|
|
1406
|
+
]
|
|
1407
|
+
self.mock_mt5.history_orders_get.return_value = mock_orders_hist_data
|
|
1408
|
+
from_date = datetime(2023, 2, 1)
|
|
1409
|
+
to_date = datetime(2023, 2, 28)
|
|
1410
|
+
history_df = self.account.get_orders_history(
|
|
1411
|
+
date_from=from_date, date_to=to_date, to_df=True
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
self.assertIsNotNone(history_df)
|
|
1415
|
+
self.assertIsInstance(history_df, pd.DataFrame)
|
|
1416
|
+
self.assertEqual(len(history_df), 2)
|
|
1417
|
+
self.assertEqual(history_df.iloc[0]["symbol"], "XAUUSD")
|
|
1418
|
+
self.mock_mt5.history_orders_get.assert_called_once_with(from_date, to_date)
|
|
1419
|
+
|
|
1420
|
+
def test_get_orders_history_by_position_as_tuple(self):
|
|
1421
|
+
mock_orders_hist_data = [self._get_mock_order(ticket=405, position_id=1001)]
|
|
1422
|
+
self.mock_mt5.history_orders_get.return_value = mock_orders_hist_data
|
|
1423
|
+
history_tuple = self.account.get_orders_history(position=1001, to_df=False)
|
|
1424
|
+
|
|
1425
|
+
self.assertIsNotNone(history_tuple)
|
|
1426
|
+
self.assertIsInstance(history_tuple, tuple)
|
|
1427
|
+
self.assertEqual(len(history_tuple), 1)
|
|
1428
|
+
self.assertEqual(history_tuple[0].position_id, 1001)
|
|
1429
|
+
self.mock_mt5.history_orders_get.assert_called_once_with(position=1001)
|
|
1430
|
+
|
|
1431
|
+
def test_get_orders_history_no_orders(self):
|
|
1432
|
+
self.mock_mt5.history_orders_get.return_value = []
|
|
1433
|
+
history = self.account.get_orders_history()
|
|
1434
|
+
self.assertIsNone(history)
|
|
1435
|
+
|
|
1436
|
+
self.mock_mt5.history_orders_get.return_value = None
|
|
1437
|
+
history = self.account.get_orders_history()
|
|
1438
|
+
self.assertIsNone(history)
|
|
1439
|
+
|
|
1440
|
+
def test_shutdown(self):
|
|
1441
|
+
self.account.shutdown()
|
|
1442
|
+
self.mock_mt5.shutdown.assert_called_once()
|
|
1443
|
+
|
|
1444
|
+
def test_check_brokers_supported(self):
|
|
1445
|
+
# This is implicitly tested by setUp, but an explicit test can be added
|
|
1446
|
+
# Ensure no InvalidBroker exception is raised for a supported broker
|
|
1447
|
+
try:
|
|
1448
|
+
# Re-initialize with a known supported broker (already done in setUp)
|
|
1449
|
+
self.mock_mt5.account_info.return_value = (
|
|
1450
|
+
self.mock_mt5.account_info.return_value._replace(
|
|
1451
|
+
company=SUPPORTED_BROKERS[0]
|
|
1452
|
+
)
|
|
1453
|
+
)
|
|
1454
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1455
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
1456
|
+
company=SUPPORTED_BROKERS[0]
|
|
1457
|
+
)
|
|
1458
|
+
)
|
|
1459
|
+
Account()
|
|
1460
|
+
except InvalidBroker:
|
|
1461
|
+
self.fail("InvalidBroker raised unexpectedly for a supported broker")
|
|
1462
|
+
|
|
1463
|
+
def test_check_brokers_unsupported(self):
|
|
1464
|
+
self.mock_mt5.account_info.return_value = (
|
|
1465
|
+
self.mock_mt5.account_info.return_value._replace(
|
|
1466
|
+
company="Unsupported Broker Inc."
|
|
1467
|
+
)
|
|
1468
|
+
)
|
|
1469
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1470
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
1471
|
+
company="Unsupported Broker Inc."
|
|
1472
|
+
)
|
|
1473
|
+
)
|
|
1474
|
+
with self.assertRaises(InvalidBroker) as context:
|
|
1475
|
+
Account()
|
|
1476
|
+
self.assertIn("is not currently supported broker", str(context.exception))
|
|
1477
|
+
|
|
1478
|
+
def test_check_brokers_copy_flag(self):
|
|
1479
|
+
self.mock_mt5.account_info.return_value = (
|
|
1480
|
+
self.mock_mt5.account_info.return_value._replace(
|
|
1481
|
+
company="Unsupported Broker Inc."
|
|
1482
|
+
)
|
|
1483
|
+
)
|
|
1484
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1485
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
1486
|
+
company="Unsupported Broker Inc."
|
|
1487
|
+
)
|
|
1488
|
+
)
|
|
1489
|
+
try:
|
|
1490
|
+
Account(copy=True) # Should not raise InvalidBroker
|
|
1491
|
+
except InvalidBroker:
|
|
1492
|
+
self.fail("InvalidBroker raised unexpectedly when copy=True")
|
|
1493
|
+
|
|
1494
|
+
def test_check_brokers_backtest_flag(self):
|
|
1495
|
+
self.mock_mt5.account_info.return_value = (
|
|
1496
|
+
self.mock_mt5.account_info.return_value._replace(
|
|
1497
|
+
company="Unsupported Broker Inc."
|
|
1498
|
+
)
|
|
1499
|
+
)
|
|
1500
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1501
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
1502
|
+
company="Unsupported Broker Inc."
|
|
1503
|
+
)
|
|
1504
|
+
)
|
|
1505
|
+
try:
|
|
1506
|
+
Account(backtest=True) # Should not raise InvalidBroker
|
|
1507
|
+
except InvalidBroker:
|
|
1508
|
+
self.fail("InvalidBroker raised unexpectedly when backtest=True")
|
|
1509
|
+
|
|
1510
|
+
def test_property_broker(self):
|
|
1511
|
+
# __BROKERS__ needs to be available in the test context.
|
|
1512
|
+
# Default company from setUp is __BROKERS__["AMG"]
|
|
1513
|
+
self.assertEqual(self.account.broker.name, __BROKERS__["AMG"])
|
|
1514
|
+
self.assertIsInstance(self.account.broker, Broker)
|
|
1515
|
+
|
|
1516
|
+
# Test with a different broker
|
|
1517
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1518
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
1519
|
+
company=__BROKERS__["JGM"]
|
|
1520
|
+
)
|
|
1521
|
+
)
|
|
1522
|
+
self.account = Account()
|
|
1523
|
+
self.assertEqual(self.account.broker.name, __BROKERS__["JGM"])
|
|
1524
|
+
self.assertIsInstance(
|
|
1525
|
+
self.account.broker, Broker
|
|
1526
|
+
) # It's always a Broker instance
|
|
1527
|
+
|
|
1528
|
+
def test_property_timezone(self):
|
|
1529
|
+
# Default broker from setUp is AdmiralMarktsGroup (AMG)
|
|
1530
|
+
self.assertEqual(self.account.timezone, AdmiralMarktsGroup().timezone)
|
|
1531
|
+
|
|
1532
|
+
# Test with FTMO
|
|
1533
|
+
self.mock_mt5.account_info.return_value = (
|
|
1534
|
+
self.mock_mt5.account_info.return_value._replace(
|
|
1535
|
+
company=__BROKERS__["FTMO"]
|
|
1536
|
+
)
|
|
1537
|
+
)
|
|
1538
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1539
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
1540
|
+
company=__BROKERS__["FTMO"]
|
|
1541
|
+
)
|
|
1542
|
+
)
|
|
1543
|
+
self.account = Account()
|
|
1544
|
+
self.assertEqual(self.account.timezone, FTMO().timezone)
|
|
1545
|
+
|
|
1546
|
+
# Test with Pepperstone
|
|
1547
|
+
self.mock_mt5.account_info.return_value = (
|
|
1548
|
+
self.mock_mt5.account_info.return_value._replace(company=__BROKERS__["PGL"])
|
|
1549
|
+
)
|
|
1550
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1551
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
1552
|
+
company=__BROKERS__["PGL"]
|
|
1553
|
+
)
|
|
1554
|
+
)
|
|
1555
|
+
self.account = Account()
|
|
1556
|
+
self.assertEqual(self.account.timezone, PepperstoneGroupLimited().timezone)
|
|
1557
|
+
|
|
1558
|
+
# Test with JustGlobalMarkets
|
|
1559
|
+
self.mock_mt5.account_info.return_value = (
|
|
1560
|
+
self.mock_mt5.account_info.return_value._replace(company=__BROKERS__["JGM"])
|
|
1561
|
+
)
|
|
1562
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1563
|
+
self.mock_mt5.terminal_info.return_value._replace(
|
|
1564
|
+
company=__BROKERS__["JGM"]
|
|
1565
|
+
)
|
|
1566
|
+
)
|
|
1567
|
+
self.account = Account()
|
|
1568
|
+
self.assertEqual(self.account.timezone, JustGlobalMarkets().timezone)
|
|
1569
|
+
|
|
1570
|
+
def test_property_name(self):
|
|
1571
|
+
# Relies on get_account_info().name
|
|
1572
|
+
self.assertEqual(self.account.name, "Test Account")
|
|
1573
|
+
self.mock_mt5.account_info.return_value = (
|
|
1574
|
+
self.mock_mt5.account_info.return_value._replace(name="Another Name")
|
|
1575
|
+
)
|
|
1576
|
+
self.assertEqual(self.account.name, "Another Name")
|
|
1577
|
+
|
|
1578
|
+
def test_property_number(self):
|
|
1579
|
+
self.assertEqual(self.account.number, 12345)
|
|
1580
|
+
self.mock_mt5.account_info.return_value = (
|
|
1581
|
+
self.mock_mt5.account_info.return_value._replace(login=99999)
|
|
1582
|
+
)
|
|
1583
|
+
self.assertEqual(self.account.number, 99999)
|
|
1584
|
+
|
|
1585
|
+
def test_property_server(self):
|
|
1586
|
+
self.assertEqual(self.account.server, "Test Server")
|
|
1587
|
+
self.mock_mt5.account_info.return_value = (
|
|
1588
|
+
self.mock_mt5.account_info.return_value._replace(server="Live Server")
|
|
1589
|
+
)
|
|
1590
|
+
self.assertEqual(self.account.server, "Live Server")
|
|
1591
|
+
|
|
1592
|
+
def test_property_balance(self):
|
|
1593
|
+
self.assertEqual(self.account.balance, 10000.0)
|
|
1594
|
+
self.mock_mt5.account_info.return_value = (
|
|
1595
|
+
self.mock_mt5.account_info.return_value._replace(balance=12345.67)
|
|
1596
|
+
)
|
|
1597
|
+
self.assertEqual(self.account.balance, 12345.67)
|
|
1598
|
+
|
|
1599
|
+
def test_property_leverage(self):
|
|
1600
|
+
self.assertEqual(self.account.leverage, 100)
|
|
1601
|
+
self.mock_mt5.account_info.return_value = (
|
|
1602
|
+
self.mock_mt5.account_info.return_value._replace(leverage=200)
|
|
1603
|
+
)
|
|
1604
|
+
self.assertEqual(self.account.leverage, 200)
|
|
1605
|
+
|
|
1606
|
+
def test_property_equity(self):
|
|
1607
|
+
self.assertEqual(self.account.equity, 10000.0)
|
|
1608
|
+
self.mock_mt5.account_info.return_value = (
|
|
1609
|
+
self.mock_mt5.account_info.return_value._replace(equity=10500.50)
|
|
1610
|
+
)
|
|
1611
|
+
self.assertEqual(self.account.equity, 10500.50)
|
|
1612
|
+
|
|
1613
|
+
def test_property_currency(self):
|
|
1614
|
+
self.assertEqual(self.account.currency, "USD")
|
|
1615
|
+
self.mock_mt5.account_info.return_value = (
|
|
1616
|
+
self.mock_mt5.account_info.return_value._replace(currency="EUR")
|
|
1617
|
+
)
|
|
1618
|
+
self.assertEqual(self.account.currency, "EUR")
|
|
1619
|
+
|
|
1620
|
+
def test_property_language(self):
|
|
1621
|
+
# Relies on get_terminal_info().language
|
|
1622
|
+
self.assertEqual(self.account.language, "en")
|
|
1623
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1624
|
+
self.mock_mt5.terminal_info.return_value._replace(language="fr")
|
|
1625
|
+
)
|
|
1626
|
+
# Re-initialize account if terminal_info is cached by the property or its underlying calls upon Account init.
|
|
1627
|
+
# self.account = Account() # Or ensure property re-fetches.
|
|
1628
|
+
self.assertEqual(self.account.language, "fr")
|
|
1629
|
+
|
|
1630
|
+
def test_property_maxbars(self):
|
|
1631
|
+
# Relies on get_terminal_info().maxbars
|
|
1632
|
+
self.assertEqual(self.account.maxbars, 100000)
|
|
1633
|
+
self.mock_mt5.terminal_info.return_value = (
|
|
1634
|
+
self.mock_mt5.terminal_info.return_value._replace(maxbars=50000)
|
|
1635
|
+
)
|
|
1636
|
+
self.assertEqual(self.account.maxbars, 50000)
|
|
1637
|
+
|
|
1638
|
+
def test_get_rate_info_success(self):
|
|
1639
|
+
mock_rate = (1678886400, 1.1, 1.1005, 1.0995, 1.1, 100, 1, 1000)
|
|
1640
|
+
self.mock_mt5.copy_rates_from_pos.return_value = [mock_rate]
|
|
1641
|
+
rate_info = self.account.get_rate_info("EURUSD", "1m")
|
|
1642
|
+
self.assertIsNotNone(rate_info)
|
|
1643
|
+
self.assertIsInstance(rate_info, RateInfo)
|
|
1644
|
+
self.assertEqual(rate_info.time, 1678886400)
|
|
1645
|
+
self.mock_mt5.copy_rates_from_pos.assert_called_once_with(
|
|
1646
|
+
"EURUSD", TIMEFRAMES["1m"], 0, 1
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
def test_get_rate_info_failure(self):
|
|
1650
|
+
self.mock_mt5.copy_rates_from_pos.return_value = None
|
|
1651
|
+
rate_info = self.account.get_rate_info("EURUSD", "1m")
|
|
1652
|
+
self.assertIsNone(rate_info)
|
|
1653
|
+
|
|
1654
|
+
def test_get_rates_from_pos_success(self):
|
|
1655
|
+
mock_rates = np.array(
|
|
1656
|
+
[
|
|
1657
|
+
(1678886400, 1.1, 1.1005, 1.0995, 1.1, 100, 1, 1000),
|
|
1658
|
+
(1678886460, 1.1, 1.1010, 1.0998, 1.1005, 120, 1, 1200),
|
|
1659
|
+
],
|
|
1660
|
+
dtype=RateDtype,
|
|
1661
|
+
)
|
|
1662
|
+
self.mock_mt5.copy_rates_from_pos.return_value = mock_rates
|
|
1663
|
+
rates = self.account.get_rates_from_pos("EURUSD", "1m", 0, 2)
|
|
1664
|
+
self.assertIsNotNone(rates)
|
|
1665
|
+
self.assertEqual(len(rates), 2)
|
|
1666
|
+
self.assertEqual(rates[0]["time"], 1678886400)
|
|
1667
|
+
self.mock_mt5.copy_rates_from_pos.assert_called_once_with(
|
|
1668
|
+
"EURUSD", TIMEFRAMES["1m"], 0, 2
|
|
1669
|
+
)
|
|
1670
|
+
|
|
1671
|
+
def test_get_rates_from_pos_failure(self):
|
|
1672
|
+
self.mock_mt5.copy_rates_from_pos.return_value = None
|
|
1673
|
+
rates = self.account.get_rates_from_pos("EURUSD", "1m", 0, 2)
|
|
1674
|
+
self.assertEqual(len(rates), 0)
|
|
1675
|
+
|
|
1676
|
+
def test_get_rates_from_date_success(self):
|
|
1677
|
+
date_from = datetime(2023, 3, 15)
|
|
1678
|
+
mock_rates = np.array(
|
|
1679
|
+
[
|
|
1680
|
+
(1678886400, 1.1, 1.1005, 1.0995, 1.1, 100, 1, 1000),
|
|
1681
|
+
],
|
|
1682
|
+
dtype=RateDtype,
|
|
1683
|
+
)
|
|
1684
|
+
self.mock_mt5.copy_rates_from.return_value = mock_rates
|
|
1685
|
+
rates = self.account.get_rates_from_date("EURUSD", "1m", date_from, 1)
|
|
1686
|
+
self.assertIsNotNone(rates)
|
|
1687
|
+
self.assertEqual(len(rates), 1)
|
|
1688
|
+
self.mock_mt5.copy_rates_from.assert_called_once_with(
|
|
1689
|
+
"EURUSD", TIMEFRAMES["1m"], date_from, 1
|
|
1690
|
+
)
|
|
1691
|
+
|
|
1692
|
+
def test_get_rates_from_date_failure(self):
|
|
1693
|
+
date_from = datetime(2023, 3, 15)
|
|
1694
|
+
self.mock_mt5.copy_rates_from.return_value = None
|
|
1695
|
+
rates = self.account.get_rates_from_date("EURUSD", "1m", date_from, 1)
|
|
1696
|
+
self.assertEqual(len(rates), 0)
|
|
1697
|
+
|
|
1698
|
+
def test_get_rates_range_success(self):
|
|
1699
|
+
date_from = datetime(2023, 3, 15)
|
|
1700
|
+
date_to = datetime(2023, 3, 16)
|
|
1701
|
+
mock_rates = np.array(
|
|
1702
|
+
[
|
|
1703
|
+
(1678886400, 1.1, 1.1005, 1.0995, 1.1, 100, 1, 1000),
|
|
1704
|
+
],
|
|
1705
|
+
dtype=RateDtype,
|
|
1706
|
+
)
|
|
1707
|
+
self.mock_mt5.copy_rates_range.return_value = mock_rates
|
|
1708
|
+
rates = self.account.get_rates_range("EURUSD", "1m", date_from, date_to)
|
|
1709
|
+
self.assertIsNotNone(rates)
|
|
1710
|
+
self.assertEqual(len(rates), 1)
|
|
1711
|
+
self.mock_mt5.copy_rates_range.assert_called_once_with(
|
|
1712
|
+
"EURUSD", TIMEFRAMES["1m"], date_from, date_to
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
def test_get_rates_range_failure(self):
|
|
1716
|
+
date_from = datetime(2023, 3, 15)
|
|
1717
|
+
date_to = datetime(2023, 3, 16)
|
|
1718
|
+
self.mock_mt5.copy_rates_range.return_value = None
|
|
1719
|
+
rates = self.account.get_rates_range("EURUSD", "1m", date_from, date_to)
|
|
1720
|
+
self.assertEqual(len(rates), 0)
|
|
1721
|
+
|
|
1722
|
+
def test_get_tick_from_date_success(self):
|
|
1723
|
+
date_from = datetime(2023, 3, 15)
|
|
1724
|
+
mock_ticks = np.array(
|
|
1725
|
+
[
|
|
1726
|
+
(1678886400, 1.1, 1.1005, 1.0995, 10, 1678886400000, 4, 10.0),
|
|
1727
|
+
],
|
|
1728
|
+
dtype=TickDtype,
|
|
1729
|
+
)
|
|
1730
|
+
self.mock_mt5.copy_ticks_from.return_value = mock_ticks
|
|
1731
|
+
ticks = self.account.get_tick_from_date("EURUSD", date_from, 1)
|
|
1732
|
+
self.assertIsNotNone(ticks)
|
|
1733
|
+
self.assertEqual(len(ticks), 1)
|
|
1734
|
+
self.mock_mt5.copy_ticks_from.assert_called_once_with(
|
|
1735
|
+
"EURUSD", date_from, 1, TickFlag["all"]
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
def test_get_tick_from_date_failure(self):
|
|
1739
|
+
date_from = datetime(2023, 3, 15)
|
|
1740
|
+
self.mock_mt5.copy_ticks_from.return_value = None
|
|
1741
|
+
ticks = self.account.get_tick_from_date("EURUSD", date_from, 1)
|
|
1742
|
+
self.assertEqual(len(ticks), 0)
|
|
1743
|
+
|
|
1744
|
+
def test_get_tick_range_success(self):
|
|
1745
|
+
date_from = datetime(2023, 3, 15)
|
|
1746
|
+
date_to = datetime(2023, 3, 16)
|
|
1747
|
+
mock_ticks = np.array(
|
|
1748
|
+
[
|
|
1749
|
+
(1678886400, 1.1, 1.1005, 1.0995, 10, 1678886400000, 4, 10.0),
|
|
1750
|
+
],
|
|
1751
|
+
dtype=TickDtype,
|
|
1752
|
+
)
|
|
1753
|
+
self.mock_mt5.copy_ticks_range.return_value = mock_ticks
|
|
1754
|
+
ticks = self.account.get_tick_range("EURUSD", date_from, date_to)
|
|
1755
|
+
self.assertIsNotNone(ticks)
|
|
1756
|
+
self.assertEqual(len(ticks), 1)
|
|
1757
|
+
self.mock_mt5.copy_ticks_range.assert_called_once_with(
|
|
1758
|
+
"EURUSD", date_from, date_to, TickFlag["all"]
|
|
1759
|
+
)
|
|
1760
|
+
|
|
1761
|
+
def test_get_tick_range_failure(self):
|
|
1762
|
+
date_from = datetime(2023, 3, 15)
|
|
1763
|
+
date_to = datetime(2023, 3, 16)
|
|
1764
|
+
self.mock_mt5.copy_ticks_range.return_value = None
|
|
1765
|
+
ticks = self.account.get_tick_range("EURUSD", date_from, date_to)
|
|
1766
|
+
self.assertEqual(len(ticks), 0)
|
|
1767
|
+
|
|
1768
|
+
if __name__ == "__main__":
|
|
1769
|
+
unittest.main()
|