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,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
|