bbstrader 0.1.7__py3-none-any.whl → 0.1.9__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/btengine/__init__.py +1 -0
- bbstrader/btengine/backtest.py +11 -9
- bbstrader/btengine/performance.py +9 -30
- bbstrader/btengine/portfolio.py +18 -10
- bbstrader/metatrader/account.py +20 -6
- bbstrader/metatrader/rates.py +6 -9
- bbstrader/metatrader/risk.py +2 -3
- bbstrader/metatrader/trade.py +306 -219
- bbstrader/metatrader/utils.py +37 -29
- bbstrader/trading/execution.py +139 -92
- bbstrader/trading/strategies.py +13 -9
- bbstrader/tseries.py +500 -494
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.9.dist-info}/METADATA +2 -1
- bbstrader-0.1.9.dist-info/RECORD +26 -0
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.9.dist-info}/WHEEL +1 -1
- bbstrader-0.1.7.dist-info/RECORD +0 -26
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.9.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.9.dist-info}/top_level.txt +0 -0
bbstrader/btengine/__init__.py
CHANGED
|
@@ -50,4 +50,5 @@ from bbstrader.btengine.event import *
|
|
|
50
50
|
from bbstrader.btengine.execution import *
|
|
51
51
|
from bbstrader.btengine.performance import *
|
|
52
52
|
from bbstrader.btengine.backtest import *
|
|
53
|
+
from bbstrader.btengine.strategy import Strategy
|
|
53
54
|
from bbstrader.btengine.portfolio import Portfolio
|
bbstrader/btengine/backtest.py
CHANGED
|
@@ -99,6 +99,7 @@ class Backtest(object):
|
|
|
99
99
|
self.fills = 0
|
|
100
100
|
|
|
101
101
|
self._generate_trading_instances()
|
|
102
|
+
self.show_equity = kwargs.get("show_equity", False)
|
|
102
103
|
|
|
103
104
|
def _generate_trading_instances(self):
|
|
104
105
|
"""
|
|
@@ -186,15 +187,16 @@ class Backtest(object):
|
|
|
186
187
|
"\n"
|
|
187
188
|
)
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
if self.show_equity:
|
|
191
|
+
print("\nCreating equity curve...")
|
|
192
|
+
print("\n[======= EQUITY CURVE =======]")
|
|
193
|
+
print(
|
|
194
|
+
tabulate(
|
|
195
|
+
self.portfolio.equity_curve.tail(10),
|
|
196
|
+
headers="keys",
|
|
197
|
+
tablefmt="outline"),
|
|
198
|
+
"\n"
|
|
199
|
+
)
|
|
198
200
|
|
|
199
201
|
def simulate_trading(self):
|
|
200
202
|
"""
|
|
@@ -24,7 +24,7 @@ __all__ = [
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def create_sharpe_ratio(returns, periods=252):
|
|
27
|
+
def create_sharpe_ratio(returns, periods=252) -> float:
|
|
28
28
|
"""
|
|
29
29
|
Create the Sharpe ratio for the strategy, based on a
|
|
30
30
|
benchmark of zero (i.e. no risk-free rate information).
|
|
@@ -36,15 +36,10 @@ def create_sharpe_ratio(returns, periods=252):
|
|
|
36
36
|
Returns:
|
|
37
37
|
S (float): Sharpe ratio
|
|
38
38
|
"""
|
|
39
|
-
|
|
40
|
-
return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)
|
|
41
|
-
else:
|
|
42
|
-
return 0.0
|
|
39
|
+
return qs.stats.sharpe(returns, periods=periods)
|
|
43
40
|
|
|
44
41
|
# Define a function to calculate the Sortino Ratio
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def create_sortino_ratio(returns, periods=252):
|
|
42
|
+
def create_sortino_ratio(returns, periods=252) -> float:
|
|
48
43
|
"""
|
|
49
44
|
Create the Sortino ratio for the strategy, based on a
|
|
50
45
|
benchmark of zero (i.e. no risk-free rate information).
|
|
@@ -56,17 +51,7 @@ def create_sortino_ratio(returns, periods=252):
|
|
|
56
51
|
Returns:
|
|
57
52
|
S (float): Sortino ratio
|
|
58
53
|
"""
|
|
59
|
-
|
|
60
|
-
annualized_return = np.power(1 + np.mean(returns), periods) - 1
|
|
61
|
-
# Calculate the downside deviation
|
|
62
|
-
downside_returns = returns.copy()
|
|
63
|
-
downside_returns[returns > 0] = 0
|
|
64
|
-
annualized_downside_std = np.std(downside_returns) * np.sqrt(periods)
|
|
65
|
-
if annualized_downside_std != 0:
|
|
66
|
-
return annualized_return / annualized_downside_std
|
|
67
|
-
else:
|
|
68
|
-
return 0.0
|
|
69
|
-
|
|
54
|
+
return qs.stats.sortino(returns, periods=periods)
|
|
70
55
|
|
|
71
56
|
def create_drawdowns(pnl):
|
|
72
57
|
"""
|
|
@@ -311,19 +296,19 @@ def plot_monthly_yearly_returns(df, title):
|
|
|
311
296
|
# Show the plot
|
|
312
297
|
plt.show()
|
|
313
298
|
|
|
314
|
-
def show_qs_stats(
|
|
299
|
+
def show_qs_stats(returns, benchmark, strategy_name, save_dir=None):
|
|
315
300
|
"""
|
|
316
301
|
Generate the full quantstats report for the strategy.
|
|
317
302
|
|
|
318
303
|
Args:
|
|
319
|
-
|
|
304
|
+
returns (pd.Serie):
|
|
320
305
|
The DataFrame containing the strategy returns and drawdowns.
|
|
321
306
|
benchmark (str):
|
|
322
307
|
The ticker symbol of the benchmark to compare the strategy to.
|
|
323
308
|
strategy_name (str): The name of the strategy.
|
|
324
309
|
"""
|
|
325
310
|
# Load the returns data
|
|
326
|
-
returns =
|
|
311
|
+
returns = returns.copy()
|
|
327
312
|
|
|
328
313
|
# Drop duplicate index entries
|
|
329
314
|
returns = returns[~returns.index.duplicated(keep='first')]
|
|
@@ -332,11 +317,5 @@ def show_qs_stats(equity_curve, benchmark, strategy_name):
|
|
|
332
317
|
qs.extend_pandas()
|
|
333
318
|
|
|
334
319
|
# Generate the full report with a benchmark
|
|
335
|
-
|
|
336
|
-
qs.reports.
|
|
337
|
-
returns['Returns'], mode='full', benchmark=benchmark)
|
|
338
|
-
qs.reports.html(
|
|
339
|
-
returns['Returns'],
|
|
340
|
-
benchmark='SPY',
|
|
341
|
-
output=f"{file}_report.html",
|
|
342
|
-
title=strategy_name)
|
|
320
|
+
qs.reports.full(returns, mode='full', benchmark=benchmark)
|
|
321
|
+
qs.reports.html(returns, benchmark=benchmark, output=save_dir, title=strategy_name)
|
bbstrader/btengine/portfolio.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
2
|
from queue import Queue
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from bbstrader.btengine.event import (
|
|
5
6
|
OrderEvent, FillEvent, MarketEvent, SignalEvent
|
|
6
7
|
)
|
|
7
8
|
from bbstrader.btengine.data import DataHandler
|
|
8
9
|
from bbstrader.btengine.performance import (
|
|
9
|
-
create_drawdowns,
|
|
10
|
-
plot_returns_and_dd,
|
|
10
|
+
create_drawdowns, create_sharpe_ratio, create_sortino_ratio,
|
|
11
|
+
plot_performance, show_qs_stats,plot_returns_and_dd,
|
|
12
|
+
plot_monthly_yearly_returns
|
|
11
13
|
)
|
|
12
14
|
import quantstats as qs
|
|
13
15
|
|
|
@@ -306,8 +308,8 @@ class Portfolio(object):
|
|
|
306
308
|
returns = self.equity_curve['Returns']
|
|
307
309
|
pnl = self.equity_curve['Equity Curve']
|
|
308
310
|
|
|
309
|
-
sharpe_ratio =
|
|
310
|
-
sortino_ratio =
|
|
311
|
+
sharpe_ratio = create_sharpe_ratio(returns, periods=self.tf)
|
|
312
|
+
sortino_ratio = create_sortino_ratio(returns, periods=self.tf)
|
|
311
313
|
drawdown, _, _ = create_drawdowns(pnl)
|
|
312
314
|
max_dd = qs.stats.max_drawdown(returns)
|
|
313
315
|
dd_details = qs.stats.drawdown_details(drawdown)
|
|
@@ -323,14 +325,20 @@ class Portfolio(object):
|
|
|
323
325
|
]
|
|
324
326
|
now = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
325
327
|
strategy_name = self.strategy_name.replace(' ', '_')
|
|
326
|
-
|
|
327
|
-
|
|
328
|
+
results_dir = Path('.backtests') / strategy_name
|
|
329
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
330
|
+
|
|
331
|
+
csv_file = f"{strategy_name}_{now}_equity.csv"
|
|
332
|
+
png_file = f'{strategy_name}_{now}_returns_heatmap.png'
|
|
333
|
+
html_file = f"{strategy_name}_{now}_report.html"
|
|
334
|
+
self.equity_curve.to_csv(results_dir / csv_file)
|
|
335
|
+
|
|
328
336
|
plot_performance(self.equity_curve, self.strategy_name)
|
|
329
|
-
plot_returns_and_dd(self.equity_curve,
|
|
330
|
-
|
|
331
|
-
qs.plots.monthly_heatmap(returns, savefig='monthly_heatmap.png')
|
|
337
|
+
plot_returns_and_dd(self.equity_curve, self.benchmark, self.strategy_name)
|
|
338
|
+
qs.plots.monthly_heatmap(returns, savefig=f"{results_dir}/{png_file}")
|
|
332
339
|
plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
|
|
333
|
-
show_qs_stats(
|
|
340
|
+
show_qs_stats(returns, self.benchmark, self.strategy_name,
|
|
341
|
+
save_dir=f"{results_dir}/{html_file}")
|
|
334
342
|
|
|
335
343
|
return stats
|
|
336
344
|
|
bbstrader/metatrader/account.py
CHANGED
|
@@ -6,10 +6,9 @@ from datetime import datetime
|
|
|
6
6
|
import MetaTrader5 as mt5
|
|
7
7
|
from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
|
|
8
8
|
from bbstrader.metatrader.utils import (
|
|
9
|
-
raise_mt5_error, AccountInfo, TerminalInfo,
|
|
9
|
+
raise_mt5_error, AccountInfo, TerminalInfo, InvalidBroker,
|
|
10
10
|
SymbolInfo, TickInfo, TradeRequest, OrderCheckResult,
|
|
11
|
-
OrderSentResult, TradePosition, TradeOrder, TradeDeal,
|
|
12
|
-
)
|
|
11
|
+
OrderSentResult, TradePosition, TradeOrder, TradeDeal,)
|
|
13
12
|
from typing import Tuple, Union, List, Dict, Any, Optional, Literal
|
|
14
13
|
|
|
15
14
|
|
|
@@ -18,6 +17,13 @@ __BROKERS__ = {
|
|
|
18
17
|
'JGM': "Just Global Markets Ltd.",
|
|
19
18
|
'FTMO': "FTMO S.R.O."
|
|
20
19
|
}
|
|
20
|
+
|
|
21
|
+
BROKERS_TIMEZONES = {
|
|
22
|
+
'AMG': 'Europe/Helsinki',
|
|
23
|
+
'JGM': 'Europe/Helsinki',
|
|
24
|
+
'FTMO': 'Europe/Helsinki'
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
_ADMIRAL_MARKETS_URL_ = "https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets"
|
|
22
28
|
_ADMIRAL_MARKETS_PRODUCTS_ = ["Stocks", "ETFs",
|
|
23
29
|
"Indices", "Commodities", "Futures", "Forex"]
|
|
@@ -48,6 +54,14 @@ _SYMBOLS_TYPE_ = {
|
|
|
48
54
|
"CRYPTO": r'\b(Cryptos?)\b'
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
def check_mt5_connection():
|
|
58
|
+
try:
|
|
59
|
+
init = mt5.initialize()
|
|
60
|
+
if not init:
|
|
61
|
+
raise_mt5_error(INIT_MSG)
|
|
62
|
+
except Exception:
|
|
63
|
+
raise_mt5_error(INIT_MSG)
|
|
64
|
+
|
|
51
65
|
class Account(object):
|
|
52
66
|
"""
|
|
53
67
|
The `Account` class is utilized to retrieve information about
|
|
@@ -85,21 +99,21 @@ class Account(object):
|
|
|
85
99
|
"""
|
|
86
100
|
|
|
87
101
|
def __init__(self):
|
|
88
|
-
|
|
89
|
-
raise_mt5_error(message=INIT_MSG)
|
|
102
|
+
check_mt5_connection()
|
|
90
103
|
self._check_brokers()
|
|
91
104
|
|
|
92
105
|
def _check_brokers(self):
|
|
93
106
|
supported = __BROKERS__.copy()
|
|
94
107
|
broker = self.get_terminal_info().company
|
|
95
108
|
if broker not in supported.values():
|
|
96
|
-
|
|
109
|
+
msg = (
|
|
97
110
|
f"{broker} is not currently supported broker for the Account() class\n"
|
|
98
111
|
f"Currently Supported brokers are: {', '.join(supported.values())}\n"
|
|
99
112
|
f"For {supported['AMG']}, See [{amg_url}]\n"
|
|
100
113
|
f"For {supported['JGM']}, See [{jgm_url}]\n"
|
|
101
114
|
f"For {supported['FTMO']}, See [{ftmo_url}]\n"
|
|
102
115
|
)
|
|
116
|
+
raise InvalidBroker(message=msg)
|
|
103
117
|
|
|
104
118
|
def get_account_info(
|
|
105
119
|
self,
|
bbstrader/metatrader/rates.py
CHANGED
|
@@ -3,9 +3,8 @@ import MetaTrader5 as Mt5
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Union, Optional
|
|
5
5
|
from bbstrader.metatrader.utils import (
|
|
6
|
-
raise_mt5_error, TimeFrame, TIMEFRAMES
|
|
7
|
-
|
|
8
|
-
from bbstrader.metatrader.account import INIT_MSG
|
|
6
|
+
raise_mt5_error, TimeFrame, TIMEFRAMES)
|
|
7
|
+
from bbstrader.metatrader.account import check_mt5_connection
|
|
9
8
|
from pandas.tseries.offsets import CustomBusinessDay
|
|
10
9
|
from pandas.tseries.holiday import USFederalHolidayCalendar
|
|
11
10
|
|
|
@@ -28,7 +27,7 @@ class Rates(object):
|
|
|
28
27
|
In your MT5 terminal, go to `Tools` -> `Options` -> `Charts` -> `Max bars in chart`.
|
|
29
28
|
|
|
30
29
|
2. The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
|
|
31
|
-
get_volume` properties
|
|
30
|
+
get_volume` properties returns data in Broker's timezone.
|
|
32
31
|
|
|
33
32
|
Example:
|
|
34
33
|
>>> rates = Rates("EURUSD", "1h")
|
|
@@ -75,9 +74,7 @@ class Rates(object):
|
|
|
75
74
|
|
|
76
75
|
|
|
77
76
|
def _mt5_initialized(self):
|
|
78
|
-
|
|
79
|
-
if not Mt5.initialize():
|
|
80
|
-
raise_mt5_error(message=INIT_MSG)
|
|
77
|
+
check_mt5_connection()
|
|
81
78
|
|
|
82
79
|
def _get_start_pos(self, index, time_frame):
|
|
83
80
|
if isinstance(index, int):
|
|
@@ -226,7 +223,7 @@ class Rates(object):
|
|
|
226
223
|
|
|
227
224
|
@property
|
|
228
225
|
def get_volume(self):
|
|
229
|
-
return self.
|
|
226
|
+
return self.__data['Volume']
|
|
230
227
|
|
|
231
228
|
def get_historical_data(
|
|
232
229
|
self,
|
|
@@ -252,7 +249,7 @@ class Rates(object):
|
|
|
252
249
|
ValueError: If the starting date is greater than the ending date.
|
|
253
250
|
|
|
254
251
|
Notes:
|
|
255
|
-
The Datetime for this method is in
|
|
252
|
+
The Datetime for this method is in Local timezone.
|
|
256
253
|
"""
|
|
257
254
|
df = self._fetch_data(date_from, date_to)
|
|
258
255
|
if save_csv and df is not None:
|
bbstrader/metatrader/risk.py
CHANGED
|
@@ -7,8 +7,7 @@ import MetaTrader5 as Mt5
|
|
|
7
7
|
from bbstrader.metatrader.account import Account
|
|
8
8
|
from bbstrader.metatrader.rates import Rates
|
|
9
9
|
from bbstrader.metatrader.utils import (
|
|
10
|
-
TIMEFRAMES, raise_mt5_error, TimeFrame
|
|
11
|
-
)
|
|
10
|
+
TIMEFRAMES, raise_mt5_error, TimeFrame)
|
|
12
11
|
from typing import List, Dict, Optional, Literal, Union, Any
|
|
13
12
|
|
|
14
13
|
|
|
@@ -342,7 +341,7 @@ class RiskManagement(Account):
|
|
|
342
341
|
"""
|
|
343
342
|
P = self.get_account_info().margin_free
|
|
344
343
|
trade_risk = self.get_trade_risk()
|
|
345
|
-
loss_allowed = P * trade_risk
|
|
344
|
+
loss_allowed = P * trade_risk / 100
|
|
346
345
|
var = self.calculate_var(c=self.var_level, tf=self.var_tf)
|
|
347
346
|
return min(var, loss_allowed)
|
|
348
347
|
|