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,439 @@
1
+ import unittest
2
+ from datetime import datetime
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from bbstrader.metatrader.trade import (
6
+ Trade,
7
+ TradeAction,
8
+ TradeSignal,
9
+ TradingMode,
10
+ create_trade_instance,
11
+ generate_signal,
12
+ )
13
+ from bbstrader.metatrader.utils import (
14
+ AccountInfo,
15
+ SymbolInfo,
16
+ TickInfo,
17
+ TradeOrder,
18
+ TradePosition,
19
+ )
20
+
21
+
22
+ class TestTrade(unittest.TestCase):
23
+ def setUp(self):
24
+ """Set up the test environment."""
25
+ self.mt5_patcher = patch("bbstrader.metatrader.trade.Mt5")
26
+ self.mock_mt5 = self.mt5_patcher.start()
27
+ self.addCleanup(self.mt5_patcher.stop)
28
+
29
+ self.logger_patcher = patch("bbstrader.metatrader.trade.log")
30
+ self.mock_logger = self.logger_patcher.start()
31
+ self.addCleanup(self.logger_patcher.stop)
32
+
33
+ patch(
34
+ "bbstrader.metatrader.risk.RiskManagement.__init__", return_value=None
35
+ ).start()
36
+ self.addCleanup(patch.stopall)
37
+
38
+ patch("bbstrader.metatrader.trade.check_mt5_connection").start()
39
+ patch("bbstrader.metatrader.trade.Trade.select_symbol").start()
40
+ patch("bbstrader.metatrader.trade.Trade.prepare_symbol").start()
41
+
42
+ self.account_info = AccountInfo(
43
+ login=12345,
44
+ balance=10000.0,
45
+ equity=10000.0,
46
+ currency="USD",
47
+ name="Test Account",
48
+ server="Test Server",
49
+ leverage=100,
50
+ trade_mode=0,
51
+ limit_orders=0,
52
+ margin_so_mode=0,
53
+ trade_allowed=True,
54
+ trade_expert=True,
55
+ margin_mode=0,
56
+ currency_digits=2,
57
+ fifo_close=False,
58
+ credit=0.0,
59
+ profit=0.0,
60
+ margin=0.0,
61
+ margin_free=10000.0,
62
+ margin_level=0.0,
63
+ margin_so_call=0.0,
64
+ margin_so_so=0.0,
65
+ margin_initial=0.0,
66
+ margin_maintenance=0.0,
67
+ assets=0.0,
68
+ liabilities=0.0,
69
+ commission_blocked=0.0,
70
+ company="MetaQuotes",
71
+ )
72
+ self.symbol_info = SymbolInfo(
73
+ name="EURUSD",
74
+ visible=True,
75
+ point=0.00001,
76
+ digits=5,
77
+ spread=10,
78
+ trade_tick_value=1.0,
79
+ trade_tick_size=0.00001,
80
+ trade_contract_size=100000,
81
+ volume_min=0.01,
82
+ volume_max=100.0,
83
+ volume_step=0.01,
84
+ bid=1.10000,
85
+ ask=1.10010,
86
+ time=datetime.now(),
87
+ custom=False,
88
+ chart_mode=0,
89
+ select=True,
90
+ session_deals=0,
91
+ session_buy_orders=0,
92
+ session_sell_orders=0,
93
+ volume=0,
94
+ volumehigh=0,
95
+ volumelow=0,
96
+ bidhigh=0,
97
+ bidlow=0,
98
+ askhigh=0,
99
+ asklow=0,
100
+ last=0,
101
+ lasthigh=0,
102
+ lastlow=0,
103
+ volume_real=0,
104
+ volumehigh_real=0,
105
+ volumelow_real=0,
106
+ option_strike=0,
107
+ trade_tick_value_profit=1.0,
108
+ trade_tick_value_loss=1.0,
109
+ trade_stops_level=0,
110
+ trade_freeze_level=0,
111
+ trade_exemode=0,
112
+ swap_mode=0,
113
+ swap_rollover3days=0,
114
+ margin_hedged_use_leg=False,
115
+ expiration_mode=0,
116
+ filling_mode=0,
117
+ order_mode=0,
118
+ order_gtc_mode=0,
119
+ option_mode=0,
120
+ option_right=0,
121
+ margin_initial=0,
122
+ margin_maintenance=0,
123
+ session_volume=0,
124
+ session_turnover=0,
125
+ session_interest=0,
126
+ session_buy_orders_volume=0,
127
+ session_sell_orders_volume=0,
128
+ session_open=0,
129
+ session_close=0,
130
+ session_aw=0,
131
+ session_price_settlement=0,
132
+ session_price_limit_min=0,
133
+ session_price_limit_max=0,
134
+ margin_hedged=0,
135
+ price_change=0,
136
+ price_volatility=0,
137
+ price_theoretical=0,
138
+ price_greeks_delta=0,
139
+ price_greeks_theta=0,
140
+ price_greeks_gamma=0,
141
+ price_greeks_vega=0,
142
+ price_greeks_rho=0,
143
+ price_greeks_omega=0,
144
+ price_sensitivity=0,
145
+ basis="",
146
+ category="",
147
+ currency_base="EUR",
148
+ currency_profit="USD",
149
+ currency_margin="EUR",
150
+ bank="",
151
+ description="Euro vs US Dollar",
152
+ exchange="",
153
+ formula="",
154
+ isin="",
155
+ page="",
156
+ path="Forex\\Majors\\EURUSD",
157
+ start_time=0,
158
+ expiration_time=0,
159
+ spread_float=True,
160
+ ticks_bookdepth=0,
161
+ trade_calc_mode=0,
162
+ trade_mode=0,
163
+ trade_accrued_interest=0.0,
164
+ trade_face_value=0.0,
165
+ trade_liquidity_rate=0.0,
166
+ volume_limit=0.0,
167
+ swap_long=0.0,
168
+ swap_short=0.0,
169
+ )
170
+ self.tick_info = TickInfo(
171
+ time=datetime.now(),
172
+ bid=1.10000,
173
+ ask=1.10010,
174
+ last=1.10005,
175
+ volume=100,
176
+ time_msc=int(datetime.now().timestamp() * 1000),
177
+ flags=6,
178
+ volume_real=100.0,
179
+ )
180
+
181
+ self.trade = Trade(symbol="EURUSD", expert_id=98181105, verbose=False)
182
+
183
+ # Manually set attributes from the patched RiskManagement parent class
184
+ self.trade.symbol_info = self.symbol_info
185
+ self.trade.account_leverage = True
186
+ self.trade.be = 10
187
+ self.trade.max_risk = 5.0
188
+ self.trade.rr = 2.0
189
+ self.trade.copy_mode = False
190
+
191
+ # Patch instance methods to return mock data
192
+ patch.object(
193
+ self.trade, "get_account_info", return_value=self.account_info
194
+ ).start()
195
+ patch.object(
196
+ self.trade, "get_symbol_info", return_value=self.symbol_info
197
+ ).start()
198
+ patch.object(self.trade, "get_tick_info", return_value=self.tick_info).start()
199
+ patch.object(self.trade, "get_lot", return_value=0.1).start()
200
+ patch.object(self.trade, "get_stop_loss", return_value=200).start()
201
+ patch.object(self.trade, "get_take_profit", return_value=300).start()
202
+ patch.object(
203
+ self.trade,
204
+ "send_order",
205
+ return_value=MagicMock(retcode=self.mock_mt5.TRADE_RETCODE_DONE, order=123),
206
+ ).start()
207
+ # This patch is crucial to prevent the UnboundLocalError in close_request
208
+ patch.object(self.trade, "check_order", return_value=True).start()
209
+
210
+ def tearDown(self):
211
+ """This method is no longer needed as addCleanup handles stopping patches."""
212
+ pass
213
+
214
+ def test_trade_signal_initialization(self):
215
+ """Test TradeSignal dataclass initialization and validation."""
216
+ signal = TradeSignal(id=1, symbol="EURUSD", action=TradeAction.BUY, price=1.2)
217
+ self.assertEqual(signal.id, 1)
218
+ self.assertEqual(signal.action, TradeAction.BUY)
219
+
220
+ with self.assertRaises(TypeError):
221
+ TradeSignal(id=1, symbol="EURUSD", action="BUY", price=1.2)
222
+
223
+ with self.assertRaises(ValueError):
224
+ TradeSignal(id=1, symbol="EURUSD", action=TradeAction.BUY, stoplimit=1.2)
225
+
226
+ def test_generate_signal(self):
227
+ """Test the generate_signal factory function."""
228
+ signal = generate_signal(
229
+ id=1, symbol="EURUSD", action=TradeAction.SELL, price=1.3
230
+ )
231
+ self.assertIsInstance(signal, TradeSignal)
232
+ self.assertEqual(signal.price, 1.3)
233
+
234
+ def test_trading_mode_enum(self):
235
+ """Test the TradingMode enum."""
236
+ self.assertTrue(TradingMode.BACKTEST.isbacktest())
237
+ self.assertFalse(TradingMode.LIVE.isbacktest())
238
+ self.assertTrue(TradingMode.LIVE.islive())
239
+ self.assertFalse(TradingMode.BACKTEST.islive())
240
+
241
+ @patch("bbstrader.metatrader.trade.tabulate")
242
+ @patch("builtins.print")
243
+ def test_summary(self, mock_print, mock_tabulate):
244
+ """Test the summary method."""
245
+ self.trade.summary()
246
+ mock_tabulate.assert_called_once()
247
+ mock_print.assert_called()
248
+
249
+ @patch("bbstrader.metatrader.trade.tabulate")
250
+ @patch("builtins.print")
251
+ def test_risk_management_summary(self, mock_print, mock_tabulate):
252
+ """Test the risk_managment method."""
253
+ with (
254
+ patch.object(
255
+ self.trade, "get_stats", return_value=({}, {"total_profit": 100})
256
+ ),
257
+ patch.object(
258
+ self.trade,
259
+ "currency_risk",
260
+ return_value={"trade_loss": 10, "trade_profit": 20},
261
+ ),
262
+ patch.object(self.trade, "get_currency_rates", return_value={"mc": "EUR"}),
263
+ patch.object(self.trade, "is_risk_ok", return_value=True),
264
+ patch.object(self.trade, "risk_level", return_value=1.0),
265
+ patch.object(self.trade, "get_leverage", return_value="1:100"),
266
+ patch.object(self.trade, "volume", return_value=0.1),
267
+ patch.object(self.trade, "get_currency_risk", return_value=100),
268
+ patch.object(self.trade, "expected_profit", return_value=200),
269
+ patch.object(self.trade, "get_break_even", return_value=50),
270
+ patch.object(self.trade, "get_deviation", return_value=20),
271
+ patch.object(self.trade, "get_minutes", return_value=60),
272
+ patch.object(self.trade, "max_trade", return_value=10),
273
+ ):
274
+ self.trade.risk_managment()
275
+ mock_tabulate.assert_called_once()
276
+ mock_print.assert_called()
277
+
278
+ @patch("pandas.DataFrame.to_csv")
279
+ @patch("os.makedirs")
280
+ def test_statistics(self, mock_makedirs, mock_to_csv):
281
+ """Test the statistics method."""
282
+ stats1 = {
283
+ "deals": 1,
284
+ "profit": 100,
285
+ "win_trades": 1,
286
+ "loss_trades": 0,
287
+ "total_fees": -10,
288
+ "average_fee": -10,
289
+ "win_rate": 100,
290
+ }
291
+ stats2 = {"total_profit": 90, "profitability": "Yes"}
292
+ with (
293
+ patch.object(self.trade, "get_stats", return_value=(stats1, stats2)),
294
+ patch.object(self.trade, "sharpe", return_value=1.5),
295
+ patch.object(self.trade, "get_currency_risk", return_value=100),
296
+ patch.object(self.trade, "expected_profit", return_value=200),
297
+ ):
298
+ self.trade.statistics(save=True, dir="test_stats")
299
+ mock_makedirs.assert_called_with("test_stats", exist_ok=True)
300
+ mock_to_csv.assert_called_once()
301
+
302
+ def test_open_buy_position(self):
303
+ """Test opening a buy position."""
304
+ with patch.object(self.trade, "check", return_value=True):
305
+ result = self.trade.open_buy_position(action="BMKT")
306
+ self.assertTrue(result)
307
+ self.trade.send_order.assert_called()
308
+
309
+ def test_open_sell_position(self):
310
+ """Test opening a sell position."""
311
+ with patch.object(self.trade, "check", return_value=True):
312
+ result = self.trade.open_sell_position(action="SMKT")
313
+ self.assertTrue(result)
314
+ self.trade.send_order.assert_called()
315
+
316
+ def test_open_position(self):
317
+ """Test the generic open_position method."""
318
+ with patch.object(self.trade, "open_buy_position") as mock_buy:
319
+ self.trade.open_position(action="BMKT")
320
+ mock_buy.assert_called_once()
321
+
322
+ with patch.object(self.trade, "open_sell_position") as mock_sell:
323
+ self.trade.open_position(action="SMKT")
324
+ mock_sell.assert_called_once()
325
+
326
+ with self.assertRaises(ValueError):
327
+ self.trade.open_position(action="INVALID_ACTION")
328
+
329
+ def test_close_position(self):
330
+ """Test closing a position."""
331
+ position = self._get_mock_position(ticket=123)
332
+ with patch.object(self.trade, "get_positions", return_value=[position]):
333
+ result = self.trade.close_position(ticket=123)
334
+ self.assertTrue(result)
335
+ self.trade.send_order.assert_called()
336
+
337
+ def test_close_order(self):
338
+ """Test closing an order."""
339
+ with patch.object(
340
+ self.trade, "close_request", return_value=True
341
+ ) as mock_close_request:
342
+ result = self.trade.close_order(ticket=456)
343
+ self.assertTrue(result)
344
+ mock_close_request.assert_called_once()
345
+
346
+ def test_modify_order(self):
347
+ """Test modifying an order."""
348
+ order = self._get_mock_order(ticket=789)
349
+ with (
350
+ patch.object(self.trade, "get_orders", return_value=[order]),
351
+ patch.object(self.trade, "check_order", return_value=True),
352
+ ):
353
+ self.trade.modify_order(ticket=789, price=1.15)
354
+ self.trade.send_order.assert_called()
355
+ call_args = self.trade.send_order.call_args[0][0]
356
+ self.assertEqual(call_args["price"], 1.15)
357
+
358
+ def test_create_trade_instance(self):
359
+ """Test the create_trade_instance factory function."""
360
+ with patch("bbstrader.metatrader.trade.Trade") as mock_trade:
361
+ params = {"expert_id": 123}
362
+ symbols = ["EURUSD", "GBPUSD"]
363
+ instances = create_trade_instance(symbols, params)
364
+ self.assertEqual(len(instances), 2)
365
+ self.assertIn("EURUSD", instances)
366
+ self.assertIn("GBPUSD", instances)
367
+ self.assertEqual(mock_trade.call_count, 2)
368
+
369
+ def _get_mock_position(
370
+ self,
371
+ ticket=1,
372
+ symbol="EURUSD",
373
+ volume=0.1,
374
+ price_open=1.1,
375
+ type=0,
376
+ magic=98181105,
377
+ profit=0.0,
378
+ ):
379
+ return TradePosition(
380
+ ticket=ticket,
381
+ time=int(datetime.now().timestamp()),
382
+ time_msc=0,
383
+ time_update=0,
384
+ time_update_msc=0,
385
+ type=type,
386
+ magic=magic,
387
+ identifier=0,
388
+ reason=0,
389
+ volume=volume,
390
+ price_open=price_open,
391
+ sl=0,
392
+ tp=0,
393
+ price_current=price_open + 0.001,
394
+ swap=0,
395
+ profit=profit,
396
+ symbol=symbol,
397
+ comment="test",
398
+ external_id="",
399
+ )
400
+
401
+ def _get_mock_order(
402
+ self,
403
+ ticket=1,
404
+ symbol="EURUSD",
405
+ price_open=1.1,
406
+ volume_initial=0.1,
407
+ type=0,
408
+ magic=98181105,
409
+ ):
410
+ return TradeOrder(
411
+ ticket=ticket,
412
+ time_setup=int(datetime.now().timestamp()),
413
+ time_setup_msc=0,
414
+ time_done=0,
415
+ time_done_msc=0,
416
+ time_expiration=0,
417
+ type=type,
418
+ type_time=0,
419
+ type_filling=0,
420
+ state=0,
421
+ magic=magic,
422
+ position_id=0,
423
+ position_by_id=0,
424
+ reason=0,
425
+ volume_initial=volume_initial,
426
+ volume_current=0.1,
427
+ price_open=price_open,
428
+ sl=0,
429
+ tp=0,
430
+ price_current=price_open,
431
+ price_stoplimit=0,
432
+ symbol=symbol,
433
+ comment="test",
434
+ external_id="",
435
+ )
436
+
437
+
438
+ if __name__ == "__main__":
439
+ unittest.main()
@@ -1 +0,0 @@
1
- bbstrader