bbstrader 0.1.6__py3-none-any.whl → 0.1.8__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/__ini__.py +0 -1
- bbstrader/btengine/__init__.py +13 -9
- bbstrader/btengine/backtest.py +99 -702
- bbstrader/btengine/data.py +3 -1
- bbstrader/btengine/event.py +15 -9
- bbstrader/btengine/execution.py +66 -6
- bbstrader/btengine/performance.py +32 -20
- bbstrader/btengine/portfolio.py +33 -15
- bbstrader/btengine/strategy.py +1 -2
- bbstrader/metatrader/account.py +15 -8
- bbstrader/metatrader/rates.py +10 -6
- bbstrader/metatrader/risk.py +1 -2
- bbstrader/metatrader/trade.py +307 -239
- bbstrader/metatrader/utils.py +37 -29
- bbstrader/models/risk.py +39 -2
- bbstrader/trading/__init__.py +8 -1
- bbstrader/trading/execution.py +378 -932
- bbstrader/trading/strategies.py +840 -0
- bbstrader/tseries.py +613 -23
- {bbstrader-0.1.6.dist-info → bbstrader-0.1.8.dist-info}/METADATA +16 -7
- bbstrader-0.1.8.dist-info/RECORD +26 -0
- {bbstrader-0.1.6.dist-info → bbstrader-0.1.8.dist-info}/WHEEL +1 -1
- bbstrader/strategies.py +0 -681
- bbstrader/trading/run.py +0 -131
- bbstrader/trading/utils.py +0 -153
- bbstrader-0.1.6.dist-info/RECORD +0 -28
- {bbstrader-0.1.6.dist-info → bbstrader-0.1.8.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.6.dist-info → bbstrader-0.1.8.dist-info}/top_level.txt +0 -0
bbstrader/btengine/data.py
CHANGED
|
@@ -360,10 +360,12 @@ class YFHistoricDataHandler(BaseCSVDataHandler):
|
|
|
360
360
|
|
|
361
361
|
|
|
362
362
|
# TODO # Get data from EODHD
|
|
363
|
+
# https://eodhd.com/
|
|
363
364
|
class EODHDHistoricDataHandler(BaseCSVDataHandler):
|
|
364
365
|
...
|
|
365
366
|
|
|
366
|
-
# TODO # Get data from
|
|
367
|
+
# TODO # Get data from FMP using Financialtoolkit API
|
|
368
|
+
# https://github.com/bbalouki/FinanceToolkit
|
|
367
369
|
class FMPHistoricDataHandler(BaseCSVDataHandler):
|
|
368
370
|
...
|
|
369
371
|
|
bbstrader/btengine/event.py
CHANGED
|
@@ -54,9 +54,10 @@ class SignalEvent(Event):
|
|
|
54
54
|
strategy_id: int,
|
|
55
55
|
symbol: str,
|
|
56
56
|
datetime: datetime,
|
|
57
|
-
signal_type:
|
|
57
|
+
signal_type: Literal['LONG', 'SHORT', 'EXIT'],
|
|
58
58
|
quantity: int | float = 100,
|
|
59
|
-
strength: int | float = 1.0
|
|
59
|
+
strength: int | float = 1.0,
|
|
60
|
+
price: int | float = None
|
|
60
61
|
):
|
|
61
62
|
"""
|
|
62
63
|
Initialises the SignalEvent.
|
|
@@ -67,10 +68,11 @@ class SignalEvent(Event):
|
|
|
67
68
|
|
|
68
69
|
symbol (str): The ticker symbol, e.g. 'GOOG'.
|
|
69
70
|
datetime (datetime): The timestamp at which the signal was generated.
|
|
70
|
-
signal_type (str): 'LONG' or 'SHORT'.
|
|
71
|
+
signal_type (str): 'LONG' or 'SHORT' or 'EXIT'.
|
|
71
72
|
quantity (int | float): An optional integer (or float) representing the order size.
|
|
72
73
|
strength (int | float): An adjustment factor "suggestion" used to scale
|
|
73
74
|
quantity at the portfolio level. Useful for pairs strategies.
|
|
75
|
+
price (int | float): An optional price to be used when the signal is generated.
|
|
74
76
|
"""
|
|
75
77
|
self.type = 'SIGNAL'
|
|
76
78
|
self.strategy_id = strategy_id
|
|
@@ -79,6 +81,7 @@ class SignalEvent(Event):
|
|
|
79
81
|
self.signal_type = signal_type
|
|
80
82
|
self.quantity = quantity
|
|
81
83
|
self.strength = strength
|
|
84
|
+
self.price = price
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
class OrderEvent(Event):
|
|
@@ -96,9 +99,10 @@ class OrderEvent(Event):
|
|
|
96
99
|
|
|
97
100
|
def __init__(self,
|
|
98
101
|
symbol: str,
|
|
99
|
-
order_type:
|
|
102
|
+
order_type: Literal['MKT', 'LMT'],
|
|
100
103
|
quantity: int | float,
|
|
101
|
-
direction:
|
|
104
|
+
direction: Literal['BUY', 'SELL'],
|
|
105
|
+
price: int | float = None
|
|
102
106
|
):
|
|
103
107
|
"""
|
|
104
108
|
Initialises the order type, setting whether it is
|
|
@@ -110,20 +114,22 @@ class OrderEvent(Event):
|
|
|
110
114
|
order_type (str): 'MKT' or 'LMT' for Market or Limit.
|
|
111
115
|
quantity (int | float): Non-negative number for quantity.
|
|
112
116
|
direction (str): 'BUY' or 'SELL' for long or short.
|
|
117
|
+
price (int | float): The price at which to order.
|
|
113
118
|
"""
|
|
114
119
|
self.type = 'ORDER'
|
|
115
120
|
self.symbol = symbol
|
|
116
121
|
self.order_type = order_type
|
|
117
122
|
self.quantity = quantity
|
|
118
123
|
self.direction = direction
|
|
124
|
+
self.price = price
|
|
119
125
|
|
|
120
126
|
def print_order(self):
|
|
121
127
|
"""
|
|
122
128
|
Outputs the values within the Order.
|
|
123
129
|
"""
|
|
124
130
|
print(
|
|
125
|
-
"Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" %
|
|
126
|
-
(self.symbol, self.order_type, self.quantity, self.direction)
|
|
131
|
+
"Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s, Price=%s" %
|
|
132
|
+
(self.symbol, self.order_type, self.quantity, self.direction, self.price)
|
|
127
133
|
)
|
|
128
134
|
|
|
129
135
|
|
|
@@ -151,7 +157,7 @@ class FillEvent(Event):
|
|
|
151
157
|
symbol: str,
|
|
152
158
|
exchange: str,
|
|
153
159
|
quantity: int | float,
|
|
154
|
-
direction: Literal['
|
|
160
|
+
direction: Literal['BUY', 'SELL'],
|
|
155
161
|
fill_cost: int | float | None,
|
|
156
162
|
commission: float | None = None
|
|
157
163
|
):
|
|
@@ -170,7 +176,7 @@ class FillEvent(Event):
|
|
|
170
176
|
exchange (str): The exchange where the order was filled.
|
|
171
177
|
quantity (int | float): The filled quantity.
|
|
172
178
|
direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
|
|
173
|
-
fill_cost (int | float):
|
|
179
|
+
fill_cost (int | float): Price of the shares when filled.
|
|
174
180
|
commission (float | None): An optional commission sent from IB.
|
|
175
181
|
"""
|
|
176
182
|
self.type = 'FILL'
|
bbstrader/btengine/execution.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import datetime
|
|
1
|
+
from datetime import datetime
|
|
2
2
|
from queue import Queue
|
|
3
3
|
from abc import ABCMeta, abstractmethod
|
|
4
4
|
from bbstrader.btengine.event import FillEvent, OrderEvent
|
|
5
|
+
from bbstrader.metatrader.account import Account
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"ExecutionHandler",
|
|
8
|
-
"SimulatedExecutionHandler"
|
|
9
|
+
"SimulatedExecutionHandler",
|
|
10
|
+
"MT5ExecutionHandler"
|
|
9
11
|
]
|
|
10
12
|
|
|
11
13
|
|
|
@@ -71,13 +73,71 @@ class SimulatedExecutionHandler(ExecutionHandler):
|
|
|
71
73
|
"""
|
|
72
74
|
if event.type == 'ORDER':
|
|
73
75
|
fill_event = FillEvent(
|
|
74
|
-
datetime.
|
|
76
|
+
datetime.now(), event.symbol,
|
|
75
77
|
'ARCA', event.quantity, event.direction, None
|
|
76
78
|
)
|
|
77
79
|
self.events.put(fill_event)
|
|
78
80
|
|
|
79
|
-
# TODO # Use in live execution
|
|
80
|
-
|
|
81
81
|
|
|
82
82
|
class MT5ExecutionHandler(ExecutionHandler):
|
|
83
|
-
|
|
83
|
+
def __init__(self, events: Queue, **kwargs):
|
|
84
|
+
"""
|
|
85
|
+
"""
|
|
86
|
+
self.events = events
|
|
87
|
+
self.account = Account()
|
|
88
|
+
|
|
89
|
+
def _estimate_total_fees(self, symbol, lot):
|
|
90
|
+
# TODO: Implement the calculation of fees based on the broker's fees
|
|
91
|
+
# https://www.metatrader5.com/en/terminal/help/trading/market_watch
|
|
92
|
+
# Calculate fees based on the broker's fees , swap and commission
|
|
93
|
+
return 0.0
|
|
94
|
+
|
|
95
|
+
def _calculate_lot(self, symbol, quantity, price):
|
|
96
|
+
FX = self.account.get_symbol_type(symbol) == 'FX'
|
|
97
|
+
COMD = self.account.get_symbol_type(symbol) == 'COMD'
|
|
98
|
+
FUT = self.account.get_symbol_type(symbol) == 'FUT'
|
|
99
|
+
CRYPTO = self.account.get_symbol_type(symbol) == 'CRYPTO'
|
|
100
|
+
symbol_info = self.account.get_symbol_info(symbol)
|
|
101
|
+
contract_size = symbol_info.trade_contract_size
|
|
102
|
+
|
|
103
|
+
lot = (quantity*price) / (contract_size * price)
|
|
104
|
+
if contract_size == 1:
|
|
105
|
+
lot = quantity
|
|
106
|
+
if COMD or FUT or CRYPTO and contract_size > 1:
|
|
107
|
+
lot = quantity / contract_size
|
|
108
|
+
if FX:
|
|
109
|
+
lot = (quantity*price / contract_size)
|
|
110
|
+
return self._check_lot(symbol, lot)
|
|
111
|
+
|
|
112
|
+
def _check_lot(self, symbol, lot):
|
|
113
|
+
symbol_info = self.account.get_symbol_info(symbol)
|
|
114
|
+
if lot < symbol_info.volume_min:
|
|
115
|
+
return symbol_info.volume_min
|
|
116
|
+
elif lot > symbol_info.volume_max:
|
|
117
|
+
return symbol_info.volume_max
|
|
118
|
+
return round(lot, 2)
|
|
119
|
+
|
|
120
|
+
def execute_order(self, event: OrderEvent):
|
|
121
|
+
"""
|
|
122
|
+
Executes an Order event by converting it into a Fill event.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
event (OrderEvent): Contains an Event object with order information.
|
|
126
|
+
"""
|
|
127
|
+
if event.type == 'ORDER':
|
|
128
|
+
symbol = event.symbol
|
|
129
|
+
direction = event.direction
|
|
130
|
+
quantity = event.quantity
|
|
131
|
+
price = event.price
|
|
132
|
+
lot = self._calculate_lot(symbol, quantity, price)
|
|
133
|
+
fees = self._estimate_total_fees(symbol, lot)
|
|
134
|
+
fill_event = FillEvent(
|
|
135
|
+
timeindex=datetime.now(), symbol=symbol,
|
|
136
|
+
exchange='MT5', quantity=quantity, direction=direction,
|
|
137
|
+
fill_cost=None, commission=fees
|
|
138
|
+
)
|
|
139
|
+
self.events.put(fill_event)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class IBExecutionHandler(ExecutionHandler):
|
|
143
|
+
...
|
|
@@ -6,9 +6,11 @@ import yfinance as yf
|
|
|
6
6
|
from scipy.stats import mstats
|
|
7
7
|
import matplotlib.pyplot as plt
|
|
8
8
|
from matplotlib.ticker import MaxNLocator
|
|
9
|
+
import quantstats as qs
|
|
9
10
|
|
|
10
11
|
import warnings
|
|
11
12
|
warnings.filterwarnings("ignore")
|
|
13
|
+
warnings.simplefilter(action='ignore', category=FutureWarning)
|
|
12
14
|
sns.set_theme()
|
|
13
15
|
|
|
14
16
|
__all__ = [
|
|
@@ -17,11 +19,12 @@ __all__ = [
|
|
|
17
19
|
"create_sharpe_ratio",
|
|
18
20
|
"create_sortino_ratio",
|
|
19
21
|
"plot_returns_and_dd",
|
|
20
|
-
"plot_monthly_yearly_returns"
|
|
22
|
+
"plot_monthly_yearly_returns",
|
|
23
|
+
"show_qs_stats"
|
|
21
24
|
]
|
|
22
25
|
|
|
23
26
|
|
|
24
|
-
def create_sharpe_ratio(returns, periods=252):
|
|
27
|
+
def create_sharpe_ratio(returns, periods=252) -> float:
|
|
25
28
|
"""
|
|
26
29
|
Create the Sharpe ratio for the strategy, based on a
|
|
27
30
|
benchmark of zero (i.e. no risk-free rate information).
|
|
@@ -33,15 +36,10 @@ def create_sharpe_ratio(returns, periods=252):
|
|
|
33
36
|
Returns:
|
|
34
37
|
S (float): Sharpe ratio
|
|
35
38
|
"""
|
|
36
|
-
|
|
37
|
-
return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)
|
|
38
|
-
else:
|
|
39
|
-
return 0.0
|
|
39
|
+
return qs.stats.sharpe(returns, periods=periods)
|
|
40
40
|
|
|
41
41
|
# Define a function to calculate the Sortino Ratio
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def create_sortino_ratio(returns, periods=252):
|
|
42
|
+
def create_sortino_ratio(returns, periods=252) -> float:
|
|
45
43
|
"""
|
|
46
44
|
Create the Sortino ratio for the strategy, based on a
|
|
47
45
|
benchmark of zero (i.e. no risk-free rate information).
|
|
@@ -53,17 +51,7 @@ def create_sortino_ratio(returns, periods=252):
|
|
|
53
51
|
Returns:
|
|
54
52
|
S (float): Sortino ratio
|
|
55
53
|
"""
|
|
56
|
-
|
|
57
|
-
annualized_return = np.power(1 + np.mean(returns), periods) - 1
|
|
58
|
-
# Calculate the downside deviation
|
|
59
|
-
downside_returns = returns.copy()
|
|
60
|
-
downside_returns[returns > 0] = 0
|
|
61
|
-
annualized_downside_std = np.std(downside_returns) * np.sqrt(periods)
|
|
62
|
-
if annualized_downside_std != 0:
|
|
63
|
-
return annualized_return / annualized_downside_std
|
|
64
|
-
else:
|
|
65
|
-
return 0.0
|
|
66
|
-
|
|
54
|
+
return qs.stats.sortino(returns, periods=periods)
|
|
67
55
|
|
|
68
56
|
def create_drawdowns(pnl):
|
|
69
57
|
"""
|
|
@@ -307,3 +295,27 @@ def plot_monthly_yearly_returns(df, title):
|
|
|
307
295
|
|
|
308
296
|
# Show the plot
|
|
309
297
|
plt.show()
|
|
298
|
+
|
|
299
|
+
def show_qs_stats(returns, benchmark, strategy_name, save_dir=None):
|
|
300
|
+
"""
|
|
301
|
+
Generate the full quantstats report for the strategy.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
returns (pd.Serie):
|
|
305
|
+
The DataFrame containing the strategy returns and drawdowns.
|
|
306
|
+
benchmark (str):
|
|
307
|
+
The ticker symbol of the benchmark to compare the strategy to.
|
|
308
|
+
strategy_name (str): The name of the strategy.
|
|
309
|
+
"""
|
|
310
|
+
# Load the returns data
|
|
311
|
+
returns = returns.copy()
|
|
312
|
+
|
|
313
|
+
# Drop duplicate index entries
|
|
314
|
+
returns = returns[~returns.index.duplicated(keep='first')]
|
|
315
|
+
|
|
316
|
+
# Extend pandas functionality with quantstats
|
|
317
|
+
qs.extend_pandas()
|
|
318
|
+
|
|
319
|
+
# Generate the full report with a benchmark
|
|
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,15 +1,17 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
10
|
+
create_drawdowns, create_sharpe_ratio, create_sortino_ratio,
|
|
11
|
+
plot_performance, show_qs_stats,plot_returns_and_dd,
|
|
12
|
+
plot_monthly_yearly_returns
|
|
12
13
|
)
|
|
14
|
+
import quantstats as qs
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class Portfolio(object):
|
|
@@ -20,7 +22,7 @@ class Portfolio(object):
|
|
|
20
22
|
sizing tools (such as the `Kelly Criterion`).
|
|
21
23
|
|
|
22
24
|
The portfolio order management system is possibly the most complex component of
|
|
23
|
-
an
|
|
25
|
+
an event driven backtester. Its role is to keep track of all current market positions
|
|
24
26
|
as well as the market value of the positions (known as the "holdings").
|
|
25
27
|
This is simply an estimate of the liquidation value of the position and is derived in part
|
|
26
28
|
from the data handling facility of the backtester.
|
|
@@ -95,7 +97,7 @@ class Portfolio(object):
|
|
|
95
97
|
self.timeframe = kwargs.get("time_frame", "D1")
|
|
96
98
|
self.trading_hours = kwargs.get("session_duration", 6.5)
|
|
97
99
|
self.benchmark = kwargs.get('benchmark', 'SPY')
|
|
98
|
-
self.strategy_name = kwargs.get('strategy_name', '
|
|
100
|
+
self.strategy_name = kwargs.get('strategy_name', '')
|
|
99
101
|
if self.timeframe not in self._tf_mapping():
|
|
100
102
|
raise ValueError(
|
|
101
103
|
f"Timeframe not supported,"
|
|
@@ -227,10 +229,10 @@ class Portfolio(object):
|
|
|
227
229
|
fill_dir = -1
|
|
228
230
|
|
|
229
231
|
# Update holdings list with new quantities
|
|
230
|
-
|
|
232
|
+
price = self.bars.get_latest_bar_value(
|
|
231
233
|
fill.symbol, "Adj Close"
|
|
232
234
|
)
|
|
233
|
-
cost = fill_dir *
|
|
235
|
+
cost = fill_dir * price * fill.quantity
|
|
234
236
|
self.current_holdings[fill.symbol] += cost
|
|
235
237
|
self.current_holdings['Commission'] += fill.commission
|
|
236
238
|
self.current_holdings['Cash'] -= (cost + fill.commission)
|
|
@@ -260,20 +262,21 @@ class Portfolio(object):
|
|
|
260
262
|
direction = signal.signal_type
|
|
261
263
|
quantity = signal.quantity
|
|
262
264
|
strength = signal.strength
|
|
265
|
+
price = signal.price
|
|
263
266
|
cur_quantity = self.current_positions[symbol]
|
|
264
267
|
|
|
265
268
|
order_type = 'MKT'
|
|
266
269
|
mkt_quantity = round(quantity * strength)
|
|
267
270
|
|
|
268
271
|
if direction == 'LONG' and cur_quantity == 0:
|
|
269
|
-
order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
|
|
272
|
+
order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY', price)
|
|
270
273
|
if direction == 'SHORT' and cur_quantity == 0:
|
|
271
|
-
order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')
|
|
274
|
+
order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL', price)
|
|
272
275
|
|
|
273
276
|
if direction == 'EXIT' and cur_quantity > 0:
|
|
274
|
-
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
|
|
277
|
+
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
|
|
275
278
|
if direction == 'EXIT' and cur_quantity < 0:
|
|
276
|
-
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
|
|
279
|
+
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
|
|
277
280
|
|
|
278
281
|
return order
|
|
279
282
|
|
|
@@ -307,7 +310,10 @@ class Portfolio(object):
|
|
|
307
310
|
|
|
308
311
|
sharpe_ratio = create_sharpe_ratio(returns, periods=self.tf)
|
|
309
312
|
sortino_ratio = create_sortino_ratio(returns, periods=self.tf)
|
|
310
|
-
drawdown,
|
|
313
|
+
drawdown, _, _ = create_drawdowns(pnl)
|
|
314
|
+
max_dd = qs.stats.max_drawdown(returns)
|
|
315
|
+
dd_details = qs.stats.drawdown_details(drawdown)
|
|
316
|
+
dd_duration = dd_details['days'].max()
|
|
311
317
|
self.equity_curve['Drawdown'] = drawdown
|
|
312
318
|
|
|
313
319
|
stats = [
|
|
@@ -317,10 +323,22 @@ class Portfolio(object):
|
|
|
317
323
|
("Max Drawdown", f"{max_dd * 100.0:.2f}%"),
|
|
318
324
|
("Drawdown Duration", f"{dd_duration}")
|
|
319
325
|
]
|
|
320
|
-
|
|
326
|
+
now = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
327
|
+
strategy_name = self.strategy_name.replace(' ', '_')
|
|
328
|
+
results_dir = Path('Backtest_Results') / 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
|
+
|
|
321
336
|
plot_performance(self.equity_curve, self.strategy_name)
|
|
322
|
-
plot_returns_and_dd(self.equity_curve,
|
|
323
|
-
|
|
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}")
|
|
324
339
|
plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
|
|
340
|
+
show_qs_stats(returns, self.benchmark, self.strategy_name,
|
|
341
|
+
save_dir=f"{results_dir}/{html_file}")
|
|
325
342
|
|
|
326
343
|
return stats
|
|
344
|
+
|
bbstrader/btengine/strategy.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from abc import ABCMeta, abstractmethod
|
|
2
2
|
from typing import Dict, Union
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
class Strategy(metaclass=ABCMeta):
|
|
6
5
|
"""
|
|
7
6
|
A `Strategy()` object encapsulates all calculation on market data
|
|
@@ -30,4 +29,4 @@ class Strategy(metaclass=ABCMeta):
|
|
|
30
29
|
"""
|
|
31
30
|
raise NotImplementedError(
|
|
32
31
|
"Should implement calculate_signals()"
|
|
33
|
-
)
|
|
32
|
+
)
|
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"]
|
|
@@ -93,13 +99,14 @@ class Account(object):
|
|
|
93
99
|
supported = __BROKERS__.copy()
|
|
94
100
|
broker = self.get_terminal_info().company
|
|
95
101
|
if broker not in supported.values():
|
|
96
|
-
|
|
102
|
+
msg = (
|
|
97
103
|
f"{broker} is not currently supported broker for the Account() class\n"
|
|
98
104
|
f"Currently Supported brokers are: {', '.join(supported.values())}\n"
|
|
99
105
|
f"For {supported['AMG']}, See [{amg_url}]\n"
|
|
100
106
|
f"For {supported['JGM']}, See [{jgm_url}]\n"
|
|
101
107
|
f"For {supported['FTMO']}, See [{ftmo_url}]\n"
|
|
102
108
|
)
|
|
109
|
+
raise InvalidBroker(message=msg)
|
|
103
110
|
|
|
104
111
|
def get_account_info(
|
|
105
112
|
self,
|
|
@@ -782,7 +789,7 @@ class Account(object):
|
|
|
782
789
|
else:
|
|
783
790
|
positions = mt5.positions_get()
|
|
784
791
|
|
|
785
|
-
if positions is None:
|
|
792
|
+
if positions is None or len(positions) == 0:
|
|
786
793
|
return None
|
|
787
794
|
if to_df:
|
|
788
795
|
df = pd.DataFrame(list(positions), columns=positions[0]._asdict())
|
|
@@ -885,7 +892,7 @@ class Account(object):
|
|
|
885
892
|
else:
|
|
886
893
|
position_deals = mt5.history_deals_get(date_from, date_to)
|
|
887
894
|
|
|
888
|
-
if position_deals is None:
|
|
895
|
+
if position_deals is None or len(position_deals) == 0:
|
|
889
896
|
return None
|
|
890
897
|
|
|
891
898
|
df = pd.DataFrame(list(position_deals),
|
|
@@ -961,7 +968,7 @@ class Account(object):
|
|
|
961
968
|
else:
|
|
962
969
|
orders = mt5.orders_get()
|
|
963
970
|
|
|
964
|
-
if orders is None:
|
|
971
|
+
if orders is None or len(orders) == 0:
|
|
965
972
|
return None
|
|
966
973
|
|
|
967
974
|
if to_df:
|
|
@@ -1064,7 +1071,7 @@ class Account(object):
|
|
|
1064
1071
|
else:
|
|
1065
1072
|
history_orders = mt5.history_orders_get(date_from, date_to)
|
|
1066
1073
|
|
|
1067
|
-
if history_orders is None:
|
|
1074
|
+
if history_orders is None or len(history_orders) == 0:
|
|
1068
1075
|
return None
|
|
1069
1076
|
|
|
1070
1077
|
df = pd.DataFrame(list(history_orders),
|
bbstrader/metatrader/rates.py
CHANGED
|
@@ -3,8 +3,7 @@ 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
|
-
)
|
|
6
|
+
raise_mt5_error, TimeFrame, TIMEFRAMES)
|
|
8
7
|
from bbstrader.metatrader.account import INIT_MSG
|
|
9
8
|
from pandas.tseries.offsets import CustomBusinessDay
|
|
10
9
|
from pandas.tseries.holiday import USFederalHolidayCalendar
|
|
@@ -22,8 +21,13 @@ class Rates(object):
|
|
|
22
21
|
and count of bars or by providing a specific date range.
|
|
23
22
|
|
|
24
23
|
Notes:
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
1. Befor using this class, ensure that the `Max bars in chart` in you terminal
|
|
25
|
+
is set to a value that is greater than the number of bars you want to retrieve
|
|
26
|
+
or just set it to Unlimited.
|
|
27
|
+
In your MT5 terminal, go to `Tools` -> `Options` -> `Charts` -> `Max bars in chart`.
|
|
28
|
+
|
|
29
|
+
2. The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
|
|
30
|
+
get_volume` properties returns data in Broker's timezone.
|
|
27
31
|
|
|
28
32
|
Example:
|
|
29
33
|
>>> rates = Rates("EURUSD", "1h")
|
|
@@ -221,7 +225,7 @@ class Rates(object):
|
|
|
221
225
|
|
|
222
226
|
@property
|
|
223
227
|
def get_volume(self):
|
|
224
|
-
return self.
|
|
228
|
+
return self.__data['Volume']
|
|
225
229
|
|
|
226
230
|
def get_historical_data(
|
|
227
231
|
self,
|
|
@@ -247,7 +251,7 @@ class Rates(object):
|
|
|
247
251
|
ValueError: If the starting date is greater than the ending date.
|
|
248
252
|
|
|
249
253
|
Notes:
|
|
250
|
-
The Datetime for this method is in
|
|
254
|
+
The Datetime for this method is in Local timezone.
|
|
251
255
|
"""
|
|
252
256
|
df = self._fetch_data(date_from, date_to)
|
|
253
257
|
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
|
|