bbstrader 0.3.4__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.

Files changed (38) hide show
  1. bbstrader/__init__.py +10 -1
  2. bbstrader/__main__.py +5 -0
  3. bbstrader/apps/_copier.py +3 -3
  4. bbstrader/btengine/strategy.py +113 -38
  5. bbstrader/compat.py +18 -10
  6. bbstrader/config.py +0 -16
  7. bbstrader/core/scripts.py +4 -3
  8. bbstrader/metatrader/account.py +51 -26
  9. bbstrader/metatrader/analysis.py +30 -16
  10. bbstrader/metatrader/copier.py +136 -58
  11. bbstrader/metatrader/trade.py +39 -45
  12. bbstrader/metatrader/utils.py +5 -4
  13. bbstrader/models/factors.py +17 -13
  14. bbstrader/models/ml.py +96 -49
  15. bbstrader/models/nlp.py +83 -66
  16. bbstrader/trading/execution.py +39 -22
  17. bbstrader/tseries.py +103 -127
  18. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/METADATA +29 -46
  19. bbstrader-0.3.6.dist-info/RECORD +62 -0
  20. bbstrader-0.3.6.dist-info/top_level.txt +3 -0
  21. docs/conf.py +56 -0
  22. tests/__init__.py +0 -0
  23. tests/engine/__init__.py +1 -0
  24. tests/engine/test_backtest.py +58 -0
  25. tests/engine/test_data.py +536 -0
  26. tests/engine/test_events.py +300 -0
  27. tests/engine/test_execution.py +219 -0
  28. tests/engine/test_portfolio.py +307 -0
  29. tests/metatrader/__init__.py +0 -0
  30. tests/metatrader/test_account.py +1769 -0
  31. tests/metatrader/test_rates.py +292 -0
  32. tests/metatrader/test_risk_management.py +700 -0
  33. tests/metatrader/test_trade.py +439 -0
  34. bbstrader-0.3.4.dist-info/RECORD +0 -49
  35. bbstrader-0.3.4.dist-info/top_level.txt +0 -1
  36. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/WHEEL +0 -0
  37. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/entry_points.txt +0 -0
  38. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,292 @@
1
+ from datetime import datetime
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import pytest
7
+
8
+ mock_mt5_module = MagicMock()
9
+ mock_mt5_module.TIMEFRAME_M1 = 1
10
+ mock_mt5_module.TIMEFRAME_D1 = 16408
11
+ mock_mt5_module.TIMEFRAME_W1 = 32769
12
+ mock_mt5_module.TIMEFRAME_H12 = 16396
13
+ mock_mt5_module.TIMEFRAME_MN1 = 49153
14
+
15
+
16
+ from bbstrader.metatrader.rates import ( # noqa: E402
17
+ Rates,
18
+ download_historical_data,
19
+ get_data_from_date, # noqa: F401
20
+ get_data_from_pos, # noqa: F401
21
+ )
22
+ from bbstrader.metatrader.utils import SymbolType # noqa: E402
23
+
24
+
25
+ @pytest.fixture
26
+ def mock_mt5_api(mocker):
27
+ """Fixture to configure the mocked MetaTrader5 API calls."""
28
+ rates_data = np.array(
29
+ [
30
+ (1672617600, 1.06, 1.09, 1.05, 1.07, 1200), # 2023-01-02 00:00:00 UTC
31
+ (1672704000, 1.07, 1.10, 1.06, 1.08, 1100), # 2023-01-03 00:00:00 UTC
32
+ (1672790400, 1.08, 1.11, 1.07, 1.09, 1300), # 2023-01-04 00:00:00 UTC
33
+ ],
34
+ dtype=[
35
+ ("time", "<i8"),
36
+ ("open", "<f8"),
37
+ ("high", "<f8"),
38
+ ("low", "<f8"),
39
+ ("close", "<f8"),
40
+ ("tick_volume", "<i8"),
41
+ ],
42
+ )
43
+
44
+ # 1. Mock for bbstrader.metatrader.rates.Mt5 (data fetching)
45
+ # This is the Mt5 alias used in rates.py
46
+ mock_rates_mt5 = mocker.patch("bbstrader.metatrader.rates.Mt5")
47
+ mock_rates_mt5.copy_rates_from_pos.return_value = rates_data
48
+ mock_rates_mt5.copy_rates_range.return_value = rates_data
49
+ mock_rates_mt5.copy_rates_from.return_value = rates_data
50
+
51
+ # 2. Mock for bbstrader.metatrader.utils.MT5 (for last_error and TIMEFRAME constants)
52
+ mock_utils_mt5 = mocker.patch("bbstrader.metatrader.utils.MT5")
53
+ mock_utils_mt5.last_error.return_value = (0, "Success") # Default success
54
+
55
+
56
+ mock_utils_mt5.TIMEFRAME_M1 = 1
57
+ mock_utils_mt5.TIMEFRAME_M2 = 2
58
+ mock_utils_mt5.TIMEFRAME_M3 = 3
59
+ mock_utils_mt5.TIMEFRAME_M4 = 4
60
+ mock_utils_mt5.TIMEFRAME_M5 = 5
61
+ mock_utils_mt5.TIMEFRAME_M6 = 6
62
+ mock_utils_mt5.TIMEFRAME_M10 = 10
63
+ mock_utils_mt5.TIMEFRAME_M12 = 12
64
+ mock_utils_mt5.TIMEFRAME_M15 = 15
65
+ mock_utils_mt5.TIMEFRAME_M20 = 20
66
+ mock_utils_mt5.TIMEFRAME_M30 = 30
67
+ mock_utils_mt5.TIMEFRAME_H1 = 16385
68
+ mock_utils_mt5.TIMEFRAME_H2 = 16386
69
+ mock_utils_mt5.TIMEFRAME_H3 = 16387
70
+ mock_utils_mt5.TIMEFRAME_H4 = 16388
71
+ mock_utils_mt5.TIMEFRAME_H6 = 16390
72
+ mock_utils_mt5.TIMEFRAME_H8 = 16392
73
+ mock_utils_mt5.TIMEFRAME_H12 = (
74
+ mock_mt5_module.TIMEFRAME_H12
75
+ ) # Use value from global mock
76
+ mock_utils_mt5.TIMEFRAME_D1 = (
77
+ mock_mt5_module.TIMEFRAME_D1
78
+ ) # Use value from global mock
79
+ mock_utils_mt5.TIMEFRAME_W1 = (
80
+ mock_mt5_module.TIMEFRAME_W1
81
+ ) # Use value from global mock
82
+ mock_utils_mt5.TIMEFRAME_MN1 = (
83
+ mock_mt5_module.TIMEFRAME_MN1
84
+ ) # Use value from global mock
85
+
86
+ # 3. Patch bbstrader.metatrader.utils.TIMEFRAMES directly to ensure it uses correct integer values
87
+ patched_timeframes = {
88
+ "1m": mock_utils_mt5.TIMEFRAME_M1,
89
+ "2m": mock_utils_mt5.TIMEFRAME_M2,
90
+ "3m": mock_utils_mt5.TIMEFRAME_M3,
91
+ "4m": mock_utils_mt5.TIMEFRAME_M4,
92
+ "5m": mock_utils_mt5.TIMEFRAME_M5,
93
+ "6m": mock_utils_mt5.TIMEFRAME_M6,
94
+ "10m": mock_utils_mt5.TIMEFRAME_M10,
95
+ "12m": mock_utils_mt5.TIMEFRAME_M12,
96
+ "15m": mock_utils_mt5.TIMEFRAME_M15,
97
+ "20m": mock_utils_mt5.TIMEFRAME_M20,
98
+ "30m": mock_utils_mt5.TIMEFRAME_M30,
99
+ "1h": mock_utils_mt5.TIMEFRAME_H1,
100
+ "2h": mock_utils_mt5.TIMEFRAME_H2,
101
+ "3h": mock_utils_mt5.TIMEFRAME_H3,
102
+ "4h": mock_utils_mt5.TIMEFRAME_H4,
103
+ "6h": mock_utils_mt5.TIMEFRAME_H6,
104
+ "8h": mock_utils_mt5.TIMEFRAME_H8,
105
+ "12h": mock_utils_mt5.TIMEFRAME_H12,
106
+ "D1": mock_utils_mt5.TIMEFRAME_D1,
107
+ "W1": mock_utils_mt5.TIMEFRAME_W1,
108
+ "MN1": mock_utils_mt5.TIMEFRAME_MN1,
109
+ }
110
+ mocker.patch.dict("bbstrader.metatrader.utils.TIMEFRAMES", patched_timeframes)
111
+
112
+ # The fixture should return the mock that is directly called by the Rates class for data fetching.
113
+ return mock_rates_mt5
114
+
115
+
116
+ @pytest.fixture
117
+ def mock_account(mocker):
118
+ """
119
+ Fixture to mock the Account class *specifically where it's used in the rates module*.
120
+ This is the key change.
121
+ """
122
+ mock_account_instance = MagicMock()
123
+ mock_account_instance.get_symbol_type.return_value = SymbolType.FOREX
124
+ mock_account_instance.get_symbol_info.return_value = MagicMock(
125
+ path="Group\\Forex\\EURUSD"
126
+ )
127
+ mock_account_instance.get_currency_rates.return_value = {"mc": "USD"}
128
+
129
+ mocker.patch(
130
+ "bbstrader.metatrader.rates.Account", return_value=mock_account_instance
131
+ )
132
+
133
+ return mock_account_instance
134
+
135
+
136
+ @pytest.fixture
137
+ def mock_check_connection(mocker):
138
+ """Fixture to mock the check_mt5_connection function."""
139
+ return mocker.patch("bbstrader.metatrader.rates.check_mt5_connection")
140
+
141
+
142
+ def test_rates_initialization(mock_mt5_api, mock_account, mock_check_connection):
143
+ """
144
+ Test the successful initialization of the Rates class with proper mocks.
145
+ """
146
+ rates = Rates(symbol="EURUSD", timeframe="D1", start_pos=0, count=100)
147
+
148
+ # 1. Check if MT5 connection was checked
149
+ mock_check_connection.assert_called_once()
150
+
151
+ # 2. Check if Account was instantiated (the patch in mock_account handles this)
152
+ # We can check its methods were NOT called since filter=False by default.
153
+ mock_account.get_symbol_type.assert_not_called()
154
+
155
+ # 3. Check if data was fetched on initialization
156
+ # Access TIMEFRAME_D1 from mock_mt5_module as it's defined there for tests
157
+ mock_mt5_api.copy_rates_from_pos.assert_called_once_with(
158
+ "EURUSD", mock_mt5_module.TIMEFRAME_D1, 0, 100
159
+ )
160
+
161
+ # 4. Check if the internal data DataFrame is correctly populated
162
+ assert isinstance(rates._Rates__data, pd.DataFrame)
163
+ assert not rates._Rates__data.empty
164
+ assert "Close" in rates._Rates__data.columns
165
+ assert rates._Rates__data.index.name == "Date"
166
+
167
+
168
+ def test_rates_initialization_invalid_timeframe():
169
+ """
170
+ Test that initializing with an invalid timeframe raises a ValueError.
171
+ """
172
+ with pytest.raises(ValueError, match="Unsupported time frame 'INVALID_TF'"):
173
+ # We don't need full mocks for this, as it fails before they are used.
174
+ Rates(symbol="EURUSD", timeframe="INVALID_TF")
175
+
176
+
177
+ def test_get_start_pos_with_string_date(
178
+ mocker, mock_check_connection, mock_account, mock_mt5_api
179
+ ):
180
+ """
181
+ Test the _get_pos_index calculation for a string date start_pos.
182
+ """
183
+ mock_dt = mocker.patch("bbstrader.metatrader.rates.datetime")
184
+ mock_dt.now.return_value = datetime(2023, 12, 31)
185
+
186
+ rates = Rates(
187
+ symbol="EURUSD", timeframe="D1", start_pos="2023-12-20", session_duration=24
188
+ )
189
+ assert rates.start_pos == 6
190
+
191
+
192
+ def test_get_historical_data(mock_mt5_api, mock_account, mock_check_connection):
193
+ """
194
+ Test the get_historical_data method.
195
+ """
196
+ rates = Rates("GBPUSD", "D1")
197
+ date_from = datetime(2023, 1, 1)
198
+ date_to = datetime(2023, 1, 31)
199
+
200
+ df = rates.get_historical_data(
201
+ date_from=date_from, date_to=date_to, lower_colnames=True
202
+ )
203
+
204
+ mock_mt5_api.copy_rates_range.assert_called_once_with(
205
+ "GBPUSD", mock_mt5_module.TIMEFRAME_D1, date_from, date_to
206
+ )
207
+ assert isinstance(df, pd.DataFrame)
208
+ assert "close" in df.columns
209
+ assert df.index.name == "date"
210
+
211
+
212
+ def test_data_filtering_for_stock(mock_mt5_api, mock_account, mock_check_connection):
213
+ """
214
+ Test the filtering mechanism for a stock symbol.
215
+ """
216
+ mock_account.get_symbol_type.return_value = SymbolType.STOCKS
217
+ mock_account.get_stocks_from_exchange.return_value = ["AAPL"]
218
+
219
+ with patch("bbstrader.metatrader.rates.AMG_EXCHANGES", ["'XNYS'"]):
220
+ rates = Rates("AAPL", "D1")
221
+ date_from = pd.Timestamp("2023-01-01")
222
+ date_to = pd.Timestamp("2023-01-04")
223
+
224
+ df = rates.get_historical_data(
225
+ date_from=date_from, date_to=date_to, filter=True
226
+ )
227
+
228
+ mock_account.get_symbol_type.assert_called()
229
+ mock_account.get_stocks_from_exchange.assert_called()
230
+
231
+ assert not df.isnull().values.any()
232
+
233
+
234
+ def test_data_filtering_with_fill_na(mock_mt5_api, mock_account, mock_check_connection):
235
+ """
236
+ Test filtering with fill_na=True for a D1 timeframe.
237
+ """
238
+ mock_account.get_symbol_type.return_value = SymbolType.FOREX # Use a 24/5 calendar
239
+
240
+ rates = Rates("EURUSD", "D1")
241
+ date_from = pd.Timestamp("2023-01-01")
242
+ date_to = pd.Timestamp("2023-01-04")
243
+
244
+ df = rates.get_historical_data(
245
+ date_from=date_from, date_to=date_to, filter=True, fill_na=True
246
+ )
247
+
248
+ # The 'us_futures' calendar (used for FOREX) is closed on Jan 2nd.
249
+ # With fill_na=True, this day should be present and filled.
250
+ assert not df.isnull().values.any()
251
+
252
+
253
+ def test_properties_access(mock_mt5_api, mock_account, mock_check_connection):
254
+ """
255
+ Test the data properties like .open, .close, .returns.
256
+ """
257
+ rates = Rates("EURUSD", "D1")
258
+
259
+ pd.testing.assert_series_equal(
260
+ rates.close, rates._Rates__data["Close"], check_names=False
261
+ )
262
+
263
+ returns = rates.returns
264
+ assert isinstance(returns, pd.Series)
265
+ assert not returns.isnull().any()
266
+ expected_return = (1.08 - 1.07) / 1.07
267
+ assert np.isclose(returns.iloc[0], expected_return)
268
+
269
+
270
+ def test_download_historical_data_wrapper(mocker):
271
+ """
272
+ Test the wrapper function to ensure it instantiates Rates and calls the correct method.
273
+ """
274
+ # Here, we mock the entire Rates class since we are testing the wrapper function, not the class itself.
275
+ mock_rates_class = mocker.patch("bbstrader.metatrader.rates.Rates")
276
+ mock_rates_instance = mock_rates_class.return_value
277
+
278
+ date_from = datetime(2022, 1, 1)
279
+
280
+ download_historical_data(
281
+ symbol="USDCAD", timeframe="H12", date_from=date_from, filter=True
282
+ )
283
+
284
+ # Check that Rates was initialized correctly
285
+ # Note: the timeframe "H12" will be passed as a string to the constructor.
286
+ mock_rates_class.assert_called_once_with("USDCAD", "H12")
287
+
288
+ # Check that the method on the instance was called correctly
289
+ mock_rates_instance.get_historical_data.assert_called_once()
290
+ call_args, call_kwargs = mock_rates_instance.get_historical_data.call_args
291
+ assert call_kwargs["date_from"] == date_from
292
+ assert call_kwargs["filter"] is True