bbstrader 0.3.5__py3-none-any.whl → 0.3.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bbstrader might be problematic. Click here for more details.

@@ -0,0 +1,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()