bbstrader 0.1.5__py3-none-any.whl → 0.1.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/__ini__.py +0 -1
- bbstrader/btengine/__init__.py +12 -9
- bbstrader/btengine/backtest.py +100 -702
- bbstrader/btengine/data.py +25 -12
- bbstrader/btengine/event.py +18 -11
- bbstrader/btengine/execution.py +67 -7
- bbstrader/btengine/performance.py +34 -1
- bbstrader/btengine/portfolio.py +24 -14
- bbstrader/btengine/strategy.py +4 -3
- bbstrader/metatrader/account.py +18 -6
- bbstrader/metatrader/rates.py +35 -12
- bbstrader/metatrader/trade.py +54 -38
- bbstrader/metatrader/utils.py +3 -2
- bbstrader/models/risk.py +39 -2
- bbstrader/trading/__init__.py +8 -1
- bbstrader/trading/execution.py +344 -923
- bbstrader/trading/strategies.py +838 -0
- bbstrader/tseries.py +603 -19
- {bbstrader-0.1.5.dist-info → bbstrader-0.1.7.dist-info}/METADATA +15 -7
- bbstrader-0.1.7.dist-info/RECORD +26 -0
- {bbstrader-0.1.5.dist-info → bbstrader-0.1.7.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.5.dist-info/RECORD +0 -28
- {bbstrader-0.1.5.dist-info → bbstrader-0.1.7.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.5.dist-info → bbstrader-0.1.7.dist-info}/top_level.txt +0 -0
bbstrader/btengine/data.py
CHANGED
|
@@ -8,6 +8,7 @@ from queue import Queue
|
|
|
8
8
|
from abc import ABCMeta, abstractmethod
|
|
9
9
|
from bbstrader.metatrader.rates import Rates
|
|
10
10
|
from bbstrader.btengine.event import MarketEvent
|
|
11
|
+
from datetime import datetime
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
@@ -260,7 +261,7 @@ class HistoricCSVDataHandler(BaseCSVDataHandler):
|
|
|
260
261
|
csv_dir = kwargs.get("csv_dir")
|
|
261
262
|
super().__init__(events, symbol_list, csv_dir)
|
|
262
263
|
|
|
263
|
-
|
|
264
|
+
|
|
264
265
|
class MT5HistoricDataHandler(BaseCSVDataHandler):
|
|
265
266
|
"""
|
|
266
267
|
Downloads historical data from MetaTrader 5 (MT5) and provides
|
|
@@ -281,19 +282,16 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
|
|
|
281
282
|
symbol_list (List[str]): A list of symbol strings to download data for.
|
|
282
283
|
**kwargs: Keyword arguments for data retrieval:
|
|
283
284
|
time_frame (str): MT5 time frame (e.g., 'D1' for daily).
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
If it set to `str`, it must be in 'YYYY-MM-DD' format and
|
|
287
|
-
session_duration (int | float): Number of trading hours per day.
|
|
285
|
+
mt5_start (datetime): Start date for historical data.
|
|
286
|
+
mt5_end (datetime): End date for historical data.
|
|
288
287
|
mt5_data (str): Directory for storing data (default: 'mt5_data').
|
|
289
288
|
|
|
290
289
|
Note:
|
|
291
290
|
Requires a working connection to an MT5 terminal.
|
|
292
291
|
"""
|
|
293
292
|
self.tf = kwargs.get('time_frame', 'D1')
|
|
294
|
-
self.
|
|
295
|
-
self.
|
|
296
|
-
self.sd = kwargs.get('session_duration', 6.5)
|
|
293
|
+
self.start = kwargs.get('mt5_start')
|
|
294
|
+
self.end = kwargs.get('mt5_end', datetime.now())
|
|
297
295
|
self.data_dir = kwargs.get('mt5_data', 'mt5_data')
|
|
298
296
|
self.symbol_list = symbol_list
|
|
299
297
|
csv_dir = self._download_data(self.data_dir)
|
|
@@ -304,8 +302,10 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
|
|
|
304
302
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
305
303
|
for symbol in self.symbol_list:
|
|
306
304
|
try:
|
|
307
|
-
rate = Rates(symbol, self.tf
|
|
308
|
-
data = rate.
|
|
305
|
+
rate = Rates(symbol=symbol, time_frame=self.tf)
|
|
306
|
+
data = rate.get_historical_data(
|
|
307
|
+
date_from=self.start, date_to=self.end
|
|
308
|
+
)
|
|
309
309
|
if data is None:
|
|
310
310
|
raise ValueError(f"No data found for {symbol}")
|
|
311
311
|
data.to_csv(data_dir / f'{symbol}.csv')
|
|
@@ -359,16 +359,29 @@ class YFHistoricDataHandler(BaseCSVDataHandler):
|
|
|
359
359
|
return cache_dir
|
|
360
360
|
|
|
361
361
|
|
|
362
|
-
# TODO # Get data from
|
|
362
|
+
# TODO # Get data from EODHD
|
|
363
|
+
# https://eodhd.com/
|
|
364
|
+
class EODHDHistoricDataHandler(BaseCSVDataHandler):
|
|
365
|
+
...
|
|
366
|
+
|
|
367
|
+
# TODO # Get data from FMP using Financialtoolkit API
|
|
368
|
+
# https://github.com/bbalouki/FinanceToolkit
|
|
363
369
|
class FMPHistoricDataHandler(BaseCSVDataHandler):
|
|
364
370
|
...
|
|
365
371
|
|
|
366
372
|
|
|
367
373
|
class BaseFMPDataHanler(object):
|
|
374
|
+
"""
|
|
375
|
+
This will serve as the base class for all other FMP data
|
|
376
|
+
that is not historical data and does not have an OHLC structure.
|
|
377
|
+
"""
|
|
368
378
|
...
|
|
369
379
|
|
|
370
380
|
|
|
371
381
|
class FMPFundamentalDataHandler(BaseFMPDataHanler):
|
|
372
382
|
...
|
|
373
383
|
|
|
374
|
-
# TODO Add other Handlers
|
|
384
|
+
# TODO Add other Handlers for FMP
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# TODO Add data Handlers for Interactive Brokers
|
bbstrader/btengine/event.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from typing import Literal
|
|
2
3
|
|
|
3
4
|
__all__ = [
|
|
4
5
|
"Event",
|
|
@@ -53,9 +54,10 @@ class SignalEvent(Event):
|
|
|
53
54
|
strategy_id: int,
|
|
54
55
|
symbol: str,
|
|
55
56
|
datetime: datetime,
|
|
56
|
-
signal_type:
|
|
57
|
+
signal_type: Literal['LONG', 'SHORT', 'EXIT'],
|
|
57
58
|
quantity: int | float = 100,
|
|
58
|
-
strength: int | float = 1.0
|
|
59
|
+
strength: int | float = 1.0,
|
|
60
|
+
price: int | float = None
|
|
59
61
|
):
|
|
60
62
|
"""
|
|
61
63
|
Initialises the SignalEvent.
|
|
@@ -66,10 +68,11 @@ class SignalEvent(Event):
|
|
|
66
68
|
|
|
67
69
|
symbol (str): The ticker symbol, e.g. 'GOOG'.
|
|
68
70
|
datetime (datetime): The timestamp at which the signal was generated.
|
|
69
|
-
signal_type (str): 'LONG' or 'SHORT'.
|
|
71
|
+
signal_type (str): 'LONG' or 'SHORT' or 'EXIT'.
|
|
70
72
|
quantity (int | float): An optional integer (or float) representing the order size.
|
|
71
73
|
strength (int | float): An adjustment factor "suggestion" used to scale
|
|
72
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.
|
|
73
76
|
"""
|
|
74
77
|
self.type = 'SIGNAL'
|
|
75
78
|
self.strategy_id = strategy_id
|
|
@@ -78,6 +81,7 @@ class SignalEvent(Event):
|
|
|
78
81
|
self.signal_type = signal_type
|
|
79
82
|
self.quantity = quantity
|
|
80
83
|
self.strength = strength
|
|
84
|
+
self.price = price
|
|
81
85
|
|
|
82
86
|
|
|
83
87
|
class OrderEvent(Event):
|
|
@@ -95,9 +99,10 @@ class OrderEvent(Event):
|
|
|
95
99
|
|
|
96
100
|
def __init__(self,
|
|
97
101
|
symbol: str,
|
|
98
|
-
order_type:
|
|
102
|
+
order_type: Literal['MKT', 'LMT'],
|
|
99
103
|
quantity: int | float,
|
|
100
|
-
direction:
|
|
104
|
+
direction: Literal['BUY', 'SELL'],
|
|
105
|
+
price: int | float = None
|
|
101
106
|
):
|
|
102
107
|
"""
|
|
103
108
|
Initialises the order type, setting whether it is
|
|
@@ -109,20 +114,22 @@ class OrderEvent(Event):
|
|
|
109
114
|
order_type (str): 'MKT' or 'LMT' for Market or Limit.
|
|
110
115
|
quantity (int | float): Non-negative number for quantity.
|
|
111
116
|
direction (str): 'BUY' or 'SELL' for long or short.
|
|
117
|
+
price (int | float): The price at which to order.
|
|
112
118
|
"""
|
|
113
119
|
self.type = 'ORDER'
|
|
114
120
|
self.symbol = symbol
|
|
115
121
|
self.order_type = order_type
|
|
116
122
|
self.quantity = quantity
|
|
117
123
|
self.direction = direction
|
|
124
|
+
self.price = price
|
|
118
125
|
|
|
119
126
|
def print_order(self):
|
|
120
127
|
"""
|
|
121
128
|
Outputs the values within the Order.
|
|
122
129
|
"""
|
|
123
130
|
print(
|
|
124
|
-
"Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" %
|
|
125
|
-
(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)
|
|
126
133
|
)
|
|
127
134
|
|
|
128
135
|
|
|
@@ -150,8 +157,8 @@ class FillEvent(Event):
|
|
|
150
157
|
symbol: str,
|
|
151
158
|
exchange: str,
|
|
152
159
|
quantity: int | float,
|
|
153
|
-
direction:
|
|
154
|
-
fill_cost: int | float,
|
|
160
|
+
direction: Literal['BUY', 'SELL'],
|
|
161
|
+
fill_cost: int | float | None,
|
|
155
162
|
commission: float | None = None
|
|
156
163
|
):
|
|
157
164
|
"""
|
|
@@ -168,8 +175,8 @@ class FillEvent(Event):
|
|
|
168
175
|
symbol (str): The instrument which was filled.
|
|
169
176
|
exchange (str): The exchange where the order was filled.
|
|
170
177
|
quantity (int | float): The filled quantity.
|
|
171
|
-
direction (str): The direction of fill ('
|
|
172
|
-
fill_cost (int | float):
|
|
178
|
+
direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
|
|
179
|
+
fill_cost (int | float): Price of the shares when filled.
|
|
173
180
|
commission (float | None): An optional commission sent from IB.
|
|
174
181
|
"""
|
|
175
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
|
|
|
@@ -51,7 +53,7 @@ class SimulatedExecutionHandler(ExecutionHandler):
|
|
|
51
53
|
handler.
|
|
52
54
|
"""
|
|
53
55
|
|
|
54
|
-
def __init__(self, events: Queue):
|
|
56
|
+
def __init__(self, events: Queue, **kwargs):
|
|
55
57
|
"""
|
|
56
58
|
Initialises the handler, setting the event queues
|
|
57
59
|
up internally.
|
|
@@ -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,7 +19,8 @@ __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
|
|
|
@@ -307,3 +310,33 @@ def plot_monthly_yearly_returns(df, title):
|
|
|
307
310
|
|
|
308
311
|
# Show the plot
|
|
309
312
|
plt.show()
|
|
313
|
+
|
|
314
|
+
def show_qs_stats(equity_curve, benchmark, strategy_name):
|
|
315
|
+
"""
|
|
316
|
+
Generate the full quantstats report for the strategy.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
equity_curve (pd.DataFrame):
|
|
320
|
+
The DataFrame containing the strategy returns and drawdowns.
|
|
321
|
+
benchmark (str):
|
|
322
|
+
The ticker symbol of the benchmark to compare the strategy to.
|
|
323
|
+
strategy_name (str): The name of the strategy.
|
|
324
|
+
"""
|
|
325
|
+
# Load the returns data
|
|
326
|
+
returns = equity_curve.copy()
|
|
327
|
+
|
|
328
|
+
# Drop duplicate index entries
|
|
329
|
+
returns = returns[~returns.index.duplicated(keep='first')]
|
|
330
|
+
|
|
331
|
+
# Extend pandas functionality with quantstats
|
|
332
|
+
qs.extend_pandas()
|
|
333
|
+
|
|
334
|
+
# Generate the full report with a benchmark
|
|
335
|
+
file = strategy_name.replace(' ', '_')
|
|
336
|
+
qs.reports.full(
|
|
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)
|
bbstrader/btengine/portfolio.py
CHANGED
|
@@ -6,10 +6,10 @@ from bbstrader.btengine.event import (
|
|
|
6
6
|
)
|
|
7
7
|
from bbstrader.btengine.data import DataHandler
|
|
8
8
|
from bbstrader.btengine.performance import (
|
|
9
|
-
create_drawdowns, plot_performance,
|
|
10
|
-
create_sharpe_ratio, create_sortino_ratio,
|
|
9
|
+
create_drawdowns, plot_performance, show_qs_stats,
|
|
11
10
|
plot_returns_and_dd, plot_monthly_yearly_returns
|
|
12
11
|
)
|
|
12
|
+
import quantstats as qs
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Portfolio(object):
|
|
@@ -20,7 +20,7 @@ class Portfolio(object):
|
|
|
20
20
|
sizing tools (such as the `Kelly Criterion`).
|
|
21
21
|
|
|
22
22
|
The portfolio order management system is possibly the most complex component of
|
|
23
|
-
an
|
|
23
|
+
an event driven backtester. Its role is to keep track of all current market positions
|
|
24
24
|
as well as the market value of the positions (known as the "holdings").
|
|
25
25
|
This is simply an estimate of the liquidation value of the position and is derived in part
|
|
26
26
|
from the data handling facility of the backtester.
|
|
@@ -95,7 +95,7 @@ class Portfolio(object):
|
|
|
95
95
|
self.timeframe = kwargs.get("time_frame", "D1")
|
|
96
96
|
self.trading_hours = kwargs.get("session_duration", 6.5)
|
|
97
97
|
self.benchmark = kwargs.get('benchmark', 'SPY')
|
|
98
|
-
self.strategy_name = kwargs.get('strategy_name', '
|
|
98
|
+
self.strategy_name = kwargs.get('strategy_name', '')
|
|
99
99
|
if self.timeframe not in self._tf_mapping():
|
|
100
100
|
raise ValueError(
|
|
101
101
|
f"Timeframe not supported,"
|
|
@@ -227,10 +227,10 @@ class Portfolio(object):
|
|
|
227
227
|
fill_dir = -1
|
|
228
228
|
|
|
229
229
|
# Update holdings list with new quantities
|
|
230
|
-
|
|
230
|
+
price = self.bars.get_latest_bar_value(
|
|
231
231
|
fill.symbol, "Adj Close"
|
|
232
232
|
)
|
|
233
|
-
cost = fill_dir *
|
|
233
|
+
cost = fill_dir * price * fill.quantity
|
|
234
234
|
self.current_holdings[fill.symbol] += cost
|
|
235
235
|
self.current_holdings['Commission'] += fill.commission
|
|
236
236
|
self.current_holdings['Cash'] -= (cost + fill.commission)
|
|
@@ -260,20 +260,21 @@ class Portfolio(object):
|
|
|
260
260
|
direction = signal.signal_type
|
|
261
261
|
quantity = signal.quantity
|
|
262
262
|
strength = signal.strength
|
|
263
|
+
price = signal.price
|
|
263
264
|
cur_quantity = self.current_positions[symbol]
|
|
264
265
|
|
|
265
266
|
order_type = 'MKT'
|
|
266
267
|
mkt_quantity = round(quantity * strength)
|
|
267
268
|
|
|
268
269
|
if direction == 'LONG' and cur_quantity == 0:
|
|
269
|
-
order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
|
|
270
|
+
order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY', price)
|
|
270
271
|
if direction == 'SHORT' and cur_quantity == 0:
|
|
271
|
-
order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')
|
|
272
|
+
order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL', price)
|
|
272
273
|
|
|
273
274
|
if direction == 'EXIT' and cur_quantity > 0:
|
|
274
|
-
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
|
|
275
|
+
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
|
|
275
276
|
if direction == 'EXIT' and cur_quantity < 0:
|
|
276
|
-
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
|
|
277
|
+
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
|
|
277
278
|
|
|
278
279
|
return order
|
|
279
280
|
|
|
@@ -305,9 +306,12 @@ class Portfolio(object):
|
|
|
305
306
|
returns = self.equity_curve['Returns']
|
|
306
307
|
pnl = self.equity_curve['Equity Curve']
|
|
307
308
|
|
|
308
|
-
sharpe_ratio =
|
|
309
|
-
sortino_ratio =
|
|
310
|
-
drawdown,
|
|
309
|
+
sharpe_ratio = qs.stats.sharpe(returns, periods=self.tf)
|
|
310
|
+
sortino_ratio = qs.stats.sortino(returns, periods=self.tf)
|
|
311
|
+
drawdown, _, _ = create_drawdowns(pnl)
|
|
312
|
+
max_dd = qs.stats.max_drawdown(returns)
|
|
313
|
+
dd_details = qs.stats.drawdown_details(drawdown)
|
|
314
|
+
dd_duration = dd_details['days'].max()
|
|
311
315
|
self.equity_curve['Drawdown'] = drawdown
|
|
312
316
|
|
|
313
317
|
stats = [
|
|
@@ -317,10 +321,16 @@ class Portfolio(object):
|
|
|
317
321
|
("Max Drawdown", f"{max_dd * 100.0:.2f}%"),
|
|
318
322
|
("Drawdown Duration", f"{dd_duration}")
|
|
319
323
|
]
|
|
320
|
-
|
|
324
|
+
now = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
325
|
+
strategy_name = self.strategy_name.replace(' ', '_')
|
|
326
|
+
file = f"{strategy_name}_{now}_equity.csv"
|
|
327
|
+
self.equity_curve.to_csv(file)
|
|
321
328
|
plot_performance(self.equity_curve, self.strategy_name)
|
|
322
329
|
plot_returns_and_dd(self.equity_curve,
|
|
323
330
|
self.benchmark, self.strategy_name)
|
|
331
|
+
qs.plots.monthly_heatmap(returns, savefig='monthly_heatmap.png')
|
|
324
332
|
plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
|
|
333
|
+
show_qs_stats(self.equity_curve, self.benchmark, self.strategy_name)
|
|
325
334
|
|
|
326
335
|
return stats
|
|
336
|
+
|
bbstrader/btengine/strategy.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABCMeta, abstractmethod
|
|
2
|
-
|
|
2
|
+
from typing import Dict, Union
|
|
3
3
|
|
|
4
4
|
class Strategy(metaclass=ABCMeta):
|
|
5
5
|
"""
|
|
@@ -22,10 +22,11 @@ class Strategy(metaclass=ABCMeta):
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
@abstractmethod
|
|
25
|
-
def calculate_signals(self):
|
|
25
|
+
def calculate_signals(self, *args, **kwargs) -> Dict[str, Union[str, None]]:
|
|
26
26
|
"""
|
|
27
27
|
Provides the mechanisms to calculate the list of signals.
|
|
28
|
+
This methods should return a dictionary of symbols and their respective signals.
|
|
28
29
|
"""
|
|
29
30
|
raise NotImplementedError(
|
|
30
31
|
"Should implement calculate_signals()"
|
|
31
|
-
)
|
|
32
|
+
)
|
bbstrader/metatrader/account.py
CHANGED
|
@@ -555,13 +555,19 @@ class Account(object):
|
|
|
555
555
|
|
|
556
556
|
Raises:
|
|
557
557
|
MT5TerminalError: A specific exception based on the error code.
|
|
558
|
+
|
|
559
|
+
Notes:
|
|
560
|
+
The `time` property is converted to a `datetime` object using Broker server time.
|
|
558
561
|
"""
|
|
559
562
|
try:
|
|
560
563
|
symbol_info = mt5.symbol_info(symbol)
|
|
561
564
|
if symbol_info is None:
|
|
562
565
|
return None
|
|
563
566
|
else:
|
|
564
|
-
|
|
567
|
+
symbol_info_dict = symbol_info._asdict()
|
|
568
|
+
time = datetime.fromtimestamp(symbol_info.time)
|
|
569
|
+
symbol_info_dict['time'] = time
|
|
570
|
+
return SymbolInfo(**symbol_info_dict)
|
|
565
571
|
except Exception as e:
|
|
566
572
|
msg = self._symbol_info_msg(symbol)
|
|
567
573
|
raise_mt5_error(message=f"{e+msg}")
|
|
@@ -595,13 +601,19 @@ class Account(object):
|
|
|
595
601
|
|
|
596
602
|
Raises:
|
|
597
603
|
MT5TerminalError: A specific exception based on the error code.
|
|
604
|
+
|
|
605
|
+
Notes:
|
|
606
|
+
The `time` property is converted to a `datetime` object using Broker server time.
|
|
598
607
|
"""
|
|
599
608
|
try:
|
|
600
609
|
tick_info = mt5.symbol_info_tick(symbol)
|
|
601
610
|
if tick_info is None:
|
|
602
611
|
return None
|
|
603
612
|
else:
|
|
604
|
-
|
|
613
|
+
info_dict = tick_info._asdict()
|
|
614
|
+
time = datetime.fromtimestamp(tick_info.time)
|
|
615
|
+
info_dict['time'] = time
|
|
616
|
+
return TickInfo(**info_dict)
|
|
605
617
|
except Exception as e:
|
|
606
618
|
msg = self._symbol_info_msg(symbol)
|
|
607
619
|
raise_mt5_error(message=f"{e+msg}")
|
|
@@ -770,7 +782,7 @@ class Account(object):
|
|
|
770
782
|
else:
|
|
771
783
|
positions = mt5.positions_get()
|
|
772
784
|
|
|
773
|
-
if positions is None:
|
|
785
|
+
if positions is None or len(positions) == 0:
|
|
774
786
|
return None
|
|
775
787
|
if to_df:
|
|
776
788
|
df = pd.DataFrame(list(positions), columns=positions[0]._asdict())
|
|
@@ -873,7 +885,7 @@ class Account(object):
|
|
|
873
885
|
else:
|
|
874
886
|
position_deals = mt5.history_deals_get(date_from, date_to)
|
|
875
887
|
|
|
876
|
-
if position_deals is None:
|
|
888
|
+
if position_deals is None or len(position_deals) == 0:
|
|
877
889
|
return None
|
|
878
890
|
|
|
879
891
|
df = pd.DataFrame(list(position_deals),
|
|
@@ -949,7 +961,7 @@ class Account(object):
|
|
|
949
961
|
else:
|
|
950
962
|
orders = mt5.orders_get()
|
|
951
963
|
|
|
952
|
-
if orders is None:
|
|
964
|
+
if orders is None or len(orders) == 0:
|
|
953
965
|
return None
|
|
954
966
|
|
|
955
967
|
if to_df:
|
|
@@ -1052,7 +1064,7 @@ class Account(object):
|
|
|
1052
1064
|
else:
|
|
1053
1065
|
history_orders = mt5.history_orders_get(date_from, date_to)
|
|
1054
1066
|
|
|
1055
|
-
if history_orders is None:
|
|
1067
|
+
if history_orders is None or len(history_orders) == 0:
|
|
1056
1068
|
return None
|
|
1057
1069
|
|
|
1058
1070
|
df = pd.DataFrame(list(history_orders),
|
bbstrader/metatrader/rates.py
CHANGED
|
@@ -21,6 +21,15 @@ class Rates(object):
|
|
|
21
21
|
flexibility in retrieving data either by specifying a starting position
|
|
22
22
|
and count of bars or by providing a specific date range.
|
|
23
23
|
|
|
24
|
+
Notes:
|
|
25
|
+
1. Befor using this class, ensure that the `Max bars in chart` in you terminal
|
|
26
|
+
is set to a value that is greater than the number of bars you want to retrieve
|
|
27
|
+
or just set it to Unlimited.
|
|
28
|
+
In your MT5 terminal, go to `Tools` -> `Options` -> `Charts` -> `Max bars in chart`.
|
|
29
|
+
|
|
30
|
+
2. The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
|
|
31
|
+
get_volume` properties return data in Broker's timezone.
|
|
32
|
+
|
|
24
33
|
Example:
|
|
25
34
|
>>> rates = Rates("EURUSD", "1h")
|
|
26
35
|
>>> df = rates.get_historical_data(
|
|
@@ -62,8 +71,14 @@ class Rates(object):
|
|
|
62
71
|
self.start_pos = self._get_start_pos(start_pos, time_frame)
|
|
63
72
|
self.count = count
|
|
64
73
|
self._mt5_initialized()
|
|
65
|
-
self.
|
|
74
|
+
self.__data = self.get_rates_from_pos()
|
|
75
|
+
|
|
66
76
|
|
|
77
|
+
def _mt5_initialized(self):
|
|
78
|
+
"""Ensures the MetaTrader 5 Terminal is initialized."""
|
|
79
|
+
if not Mt5.initialize():
|
|
80
|
+
raise_mt5_error(message=INIT_MSG)
|
|
81
|
+
|
|
67
82
|
def _get_start_pos(self, index, time_frame):
|
|
68
83
|
if isinstance(index, int):
|
|
69
84
|
start_pos = index
|
|
@@ -112,11 +127,6 @@ class Rates(object):
|
|
|
112
127
|
)
|
|
113
128
|
return TIMEFRAMES[time_frame]
|
|
114
129
|
|
|
115
|
-
def _mt5_initialized(self):
|
|
116
|
-
"""Ensures the MetaTrader 5 Terminal is initialized."""
|
|
117
|
-
if not Mt5.initialize():
|
|
118
|
-
raise_mt5_error(message=INIT_MSG)
|
|
119
|
-
|
|
120
130
|
def _fetch_data(
|
|
121
131
|
self, start: Union[int, datetime],
|
|
122
132
|
count: Union[int, datetime]
|
|
@@ -160,6 +170,13 @@ class Rates(object):
|
|
|
160
170
|
Returns:
|
|
161
171
|
Union[pd.DataFrame, None]: A DataFrame containing historical
|
|
162
172
|
data if successful, otherwise None.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: If `start_pos` or `count` is not provided during
|
|
176
|
+
initialization.
|
|
177
|
+
|
|
178
|
+
Notes:
|
|
179
|
+
The Datetime for this method is in Broker's timezone.
|
|
163
180
|
"""
|
|
164
181
|
if self.start_pos is None or self.count is None:
|
|
165
182
|
raise ValueError(
|
|
@@ -171,23 +188,23 @@ class Rates(object):
|
|
|
171
188
|
|
|
172
189
|
@property
|
|
173
190
|
def get_open(self):
|
|
174
|
-
return self.
|
|
191
|
+
return self.__data['Open']
|
|
175
192
|
|
|
176
193
|
@property
|
|
177
194
|
def get_high(self):
|
|
178
|
-
return self.
|
|
195
|
+
return self.__data['High']
|
|
179
196
|
|
|
180
197
|
@property
|
|
181
198
|
def get_low(self):
|
|
182
|
-
return self.
|
|
199
|
+
return self.__data['Low']
|
|
183
200
|
|
|
184
201
|
@property
|
|
185
202
|
def get_close(self):
|
|
186
|
-
return self.
|
|
203
|
+
return self.__data['Close']
|
|
187
204
|
|
|
188
205
|
@property
|
|
189
206
|
def get_adj_close(self):
|
|
190
|
-
return self.
|
|
207
|
+
return self.__data['Adj Close']
|
|
191
208
|
|
|
192
209
|
@property
|
|
193
210
|
def get_returns(self):
|
|
@@ -202,7 +219,7 @@ class Rates(object):
|
|
|
202
219
|
It calculates fractional change (also known as `per unit change or relative change`)
|
|
203
220
|
and `not percentage change`. If you need the percentage change, multiply these values by 100.
|
|
204
221
|
"""
|
|
205
|
-
data = self.
|
|
222
|
+
data = self.__data.copy()
|
|
206
223
|
data['Returns'] = data['Adj Close'].pct_change()
|
|
207
224
|
data = data.dropna()
|
|
208
225
|
return data['Returns']
|
|
@@ -230,6 +247,12 @@ class Rates(object):
|
|
|
230
247
|
Returns:
|
|
231
248
|
Union[pd.DataFrame, None]: A DataFrame containing historical data
|
|
232
249
|
if successful, otherwise None.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
ValueError: If the starting date is greater than the ending date.
|
|
253
|
+
|
|
254
|
+
Notes:
|
|
255
|
+
The Datetime for this method is in UTC timezone.
|
|
233
256
|
"""
|
|
234
257
|
df = self._fetch_data(date_from, date_to)
|
|
235
258
|
if save_csv and df is not None:
|