bbstrader 0.1.91__py3-none-any.whl → 0.1.92__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/backtest.py +11 -4
- bbstrader/btengine/data.py +10 -2
- bbstrader/btengine/event.py +10 -4
- bbstrader/btengine/execution.py +6 -6
- bbstrader/btengine/portfolio.py +51 -25
- bbstrader/btengine/strategy.py +101 -42
- bbstrader/metatrader/rates.py +50 -2
- bbstrader/metatrader/trade.py +7 -4
- bbstrader/models/optimization.py +170 -0
- bbstrader/models/portfolios.py +202 -0
- bbstrader/trading/execution.py +11 -9
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.92.dist-info}/METADATA +2 -1
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.92.dist-info}/RECORD +16 -15
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.92.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.92.dist-info}/WHEEL +0 -0
- {bbstrader-0.1.91.dist-info → bbstrader-0.1.92.dist-info}/top_level.txt +0 -0
bbstrader/btengine/backtest.py
CHANGED
|
@@ -111,8 +111,9 @@ class BacktestEngine(Backtest):
|
|
|
111
111
|
their class types.
|
|
112
112
|
"""
|
|
113
113
|
print(
|
|
114
|
-
f"\
|
|
115
|
-
f"
|
|
114
|
+
f"\n[======= STARTING BACKTEST =======]\n"
|
|
115
|
+
f"START DATE: {self.start_date} \n"
|
|
116
|
+
f"INITIAL CAPITAL: {self.initial_capital}\n"
|
|
116
117
|
)
|
|
117
118
|
self.data_handler: DataHandler = self.dh_cls(
|
|
118
119
|
self.events, self.symbol_list, **self.kwargs
|
|
@@ -141,8 +142,9 @@ class BacktestEngine(Backtest):
|
|
|
141
142
|
self.data_handler.update_bars()
|
|
142
143
|
self.strategy.check_pending_orders()
|
|
143
144
|
else:
|
|
144
|
-
print("\n[======= BACKTEST
|
|
145
|
-
print(f"
|
|
145
|
+
print("\n[======= BACKTEST COMPLETED =======]")
|
|
146
|
+
print(f"END DATE: {self.data_handler.get_latest_bar_datetime()}")
|
|
147
|
+
print(f"TOTAL BARS: {i} ")
|
|
146
148
|
break
|
|
147
149
|
|
|
148
150
|
# Handle the events
|
|
@@ -168,6 +170,11 @@ class BacktestEngine(Backtest):
|
|
|
168
170
|
elif event.type == 'FILL':
|
|
169
171
|
self.fills += 1
|
|
170
172
|
self.portfolio.update_fill(event)
|
|
173
|
+
self.strategy.update_trades_from_fill(event)
|
|
174
|
+
self.strategy.get_update_from_portfolio(
|
|
175
|
+
self.portfolio.current_positions,
|
|
176
|
+
self.portfolio.current_holdings
|
|
177
|
+
)
|
|
171
178
|
|
|
172
179
|
time.sleep(self.heartbeat)
|
|
173
180
|
|
bbstrader/btengine/data.py
CHANGED
|
@@ -272,7 +272,11 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
272
272
|
print("Symbol not available in the historical data set.")
|
|
273
273
|
raise
|
|
274
274
|
else:
|
|
275
|
-
|
|
275
|
+
try:
|
|
276
|
+
return getattr(bars_list[-1][1], val_type)
|
|
277
|
+
except AttributeError:
|
|
278
|
+
print(f"Value type {val_type} not available in the historical data set.")
|
|
279
|
+
raise
|
|
276
280
|
|
|
277
281
|
def get_latest_bars_values(self, symbol: str, val_type: str, N=1) -> np.ndarray:
|
|
278
282
|
"""
|
|
@@ -285,7 +289,11 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
285
289
|
print("That symbol is not available in the historical data set.")
|
|
286
290
|
raise
|
|
287
291
|
else:
|
|
288
|
-
|
|
292
|
+
try:
|
|
293
|
+
return np.array([getattr(b[1], val_type) for b in bars_list])
|
|
294
|
+
except AttributeError:
|
|
295
|
+
print(f"Value type {val_type} not available in the historical data set.")
|
|
296
|
+
raise
|
|
289
297
|
|
|
290
298
|
def update_bars(self):
|
|
291
299
|
"""
|
bbstrader/btengine/event.py
CHANGED
|
@@ -102,14 +102,15 @@ class OrderEvent(Event):
|
|
|
102
102
|
|
|
103
103
|
def __init__(self,
|
|
104
104
|
symbol: str,
|
|
105
|
-
order_type: Literal['MKT', 'LMT'],
|
|
105
|
+
order_type: Literal['MKT', 'LMT', 'STP', 'STPLMT'],
|
|
106
106
|
quantity: int | float,
|
|
107
107
|
direction: Literal['BUY', 'SELL'],
|
|
108
|
-
price: int | float = None
|
|
108
|
+
price: int | float = None,
|
|
109
|
+
signal: str = None
|
|
109
110
|
):
|
|
110
111
|
"""
|
|
111
112
|
Initialises the order type, setting whether it is
|
|
112
|
-
a Market order ('MKT') or Limit order ('LMT'),
|
|
113
|
+
a Market order ('MKT') or Limit order ('LMT'), or Stop order ('STP').
|
|
113
114
|
a quantity (integral or float) and its direction ('BUY' or 'SELL').
|
|
114
115
|
|
|
115
116
|
Args:
|
|
@@ -118,6 +119,7 @@ class OrderEvent(Event):
|
|
|
118
119
|
quantity (int | float): Non-negative number for quantity.
|
|
119
120
|
direction (str): 'BUY' or 'SELL' for long or short.
|
|
120
121
|
price (int | float): The price at which to order.
|
|
122
|
+
signal (str): The signal that generated the order.
|
|
121
123
|
"""
|
|
122
124
|
self.type = 'ORDER'
|
|
123
125
|
self.symbol = symbol
|
|
@@ -125,6 +127,7 @@ class OrderEvent(Event):
|
|
|
125
127
|
self.quantity = quantity
|
|
126
128
|
self.direction = direction
|
|
127
129
|
self.price = price
|
|
130
|
+
self.signal = signal
|
|
128
131
|
|
|
129
132
|
def print_order(self):
|
|
130
133
|
"""
|
|
@@ -162,7 +165,8 @@ class FillEvent(Event):
|
|
|
162
165
|
quantity: int | float,
|
|
163
166
|
direction: Literal['BUY', 'SELL'],
|
|
164
167
|
fill_cost: int | float | None,
|
|
165
|
-
commission: float | None = None
|
|
168
|
+
commission: float | None = None,
|
|
169
|
+
order: str = None
|
|
166
170
|
):
|
|
167
171
|
"""
|
|
168
172
|
Initialises the FillEvent object. Sets the symbol, exchange,
|
|
@@ -181,6 +185,7 @@ class FillEvent(Event):
|
|
|
181
185
|
direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
|
|
182
186
|
fill_cost (int | float): Price of the shares when filled.
|
|
183
187
|
commission (float | None): An optional commission sent from IB.
|
|
188
|
+
order (str): The order that this fill is related
|
|
184
189
|
"""
|
|
185
190
|
self.type = 'FILL'
|
|
186
191
|
self.timeindex = timeindex
|
|
@@ -194,6 +199,7 @@ class FillEvent(Event):
|
|
|
194
199
|
self.commission = self.calculate_ib_commission()
|
|
195
200
|
else:
|
|
196
201
|
self.commission = commission
|
|
202
|
+
self.order = order
|
|
197
203
|
|
|
198
204
|
def calculate_ib_commission(self):
|
|
199
205
|
"""
|
bbstrader/btengine/execution.py
CHANGED
|
@@ -4,7 +4,6 @@ from abc import ABCMeta, abstractmethod
|
|
|
4
4
|
from bbstrader.btengine.event import FillEvent, OrderEvent
|
|
5
5
|
from bbstrader.btengine.data import DataHandler
|
|
6
6
|
from bbstrader.metatrader.account import Account
|
|
7
|
-
from bbstrader.config import config_logger
|
|
8
7
|
|
|
9
8
|
__all__ = [
|
|
10
9
|
"ExecutionHandler",
|
|
@@ -26,7 +25,8 @@ class ExecutionHandler(metaclass=ABCMeta):
|
|
|
26
25
|
|
|
27
26
|
The ExecutionHandler described here is exceedingly simple,
|
|
28
27
|
since it fills all orders at the current market price.
|
|
29
|
-
This is highly unrealistic,
|
|
28
|
+
This is highly unrealistic, for other markets thant ``CFDs``
|
|
29
|
+
but serves as a good baseline for improvement.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
@abstractmethod
|
|
@@ -64,7 +64,7 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
64
64
|
"""
|
|
65
65
|
self.events = events
|
|
66
66
|
self.bardata = data
|
|
67
|
-
self.logger = kwargs.get("logger"
|
|
67
|
+
self.logger = kwargs.get("logger")
|
|
68
68
|
|
|
69
69
|
def execute_order(self, event: OrderEvent):
|
|
70
70
|
"""
|
|
@@ -78,7 +78,7 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
78
78
|
dtime = self.bardata.get_latest_bar_datetime(event.symbol)
|
|
79
79
|
fill_event = FillEvent(
|
|
80
80
|
dtime, event.symbol,
|
|
81
|
-
'ARCA', event.quantity, event.direction,
|
|
81
|
+
'ARCA', event.quantity, event.direction, order=event.signal
|
|
82
82
|
)
|
|
83
83
|
self.events.put(fill_event)
|
|
84
84
|
self.logger.info(
|
|
@@ -121,7 +121,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
121
121
|
"""
|
|
122
122
|
self.events = events
|
|
123
123
|
self.bardata = data
|
|
124
|
-
self.logger = kwargs.get("logger"
|
|
124
|
+
self.logger = kwargs.get("logger")
|
|
125
125
|
self.__account = Account()
|
|
126
126
|
|
|
127
127
|
def _calculate_lot(self, symbol, quantity, price):
|
|
@@ -232,7 +232,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
232
232
|
fill_event = FillEvent(
|
|
233
233
|
timeindex=dtime, symbol=symbol,
|
|
234
234
|
exchange='MT5', quantity=quantity, direction=direction,
|
|
235
|
-
fill_cost=None, commission=fees
|
|
235
|
+
fill_cost=None, commission=fees, order=event.signal
|
|
236
236
|
)
|
|
237
237
|
self.events.put(fill_event)
|
|
238
238
|
self.logger.info(
|
bbstrader/btengine/portfolio.py
CHANGED
|
@@ -31,8 +31,6 @@ class Portfolio(object):
|
|
|
31
31
|
"""
|
|
32
32
|
This describes a `Portfolio()` object that keeps track of the positions
|
|
33
33
|
within a portfolio and generates orders of a fixed quantity of stock based on signals.
|
|
34
|
-
More sophisticated portfolio objects could include risk management and position
|
|
35
|
-
sizing tools (such as the `Kelly Criterion`).
|
|
36
34
|
|
|
37
35
|
The portfolio order management system is possibly the most complex component of
|
|
38
36
|
an event driven backtester. Its role is to keep track of all current market positions
|
|
@@ -57,10 +55,9 @@ class Portfolio(object):
|
|
|
57
55
|
value (defaulting to `100,000 USD`) and others parameter based on the `Strategy` requirement.
|
|
58
56
|
|
|
59
57
|
The `Portfolio` is designed to handle position sizing and current holdings,
|
|
60
|
-
but will carry out trading orders
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
management system (OMS) functions in an eventdriven fashion.
|
|
58
|
+
but will carry out trading orders by simply them to the brokerage with a predetermined
|
|
59
|
+
fixed quantity size, if the portfolio has enough cash to place the order.
|
|
60
|
+
|
|
64
61
|
|
|
65
62
|
The portfolio contains the `all_positions` and `current_positions` members.
|
|
66
63
|
The former stores a list of all previous positions recorded at the timestamp of a market data event.
|
|
@@ -108,7 +105,7 @@ class Portfolio(object):
|
|
|
108
105
|
self.initial_capital = initial_capital
|
|
109
106
|
|
|
110
107
|
self.timeframe = kwargs.get("time_frame", "D1")
|
|
111
|
-
self.trading_hours = kwargs.get("session_duration",
|
|
108
|
+
self.trading_hours = kwargs.get("session_duration", 23)
|
|
112
109
|
self.benchmark = kwargs.get('benchmark', 'SPY')
|
|
113
110
|
self.output_dir = kwargs.get('output_dir', None)
|
|
114
111
|
self.strategy_name = kwargs.get('strategy_name', '')
|
|
@@ -174,6 +171,23 @@ class Portfolio(object):
|
|
|
174
171
|
d['Commission'] = 0.0
|
|
175
172
|
d['Total'] = self.initial_capital
|
|
176
173
|
return d
|
|
174
|
+
|
|
175
|
+
def _get_price(self, symbol: str) -> float:
|
|
176
|
+
try:
|
|
177
|
+
price = self.bars.get_latest_bar_value(
|
|
178
|
+
symbol, "adj_close"
|
|
179
|
+
)
|
|
180
|
+
return price
|
|
181
|
+
except AttributeError:
|
|
182
|
+
try:
|
|
183
|
+
price = self.bars.get_latest_bar_value(
|
|
184
|
+
symbol, "close"
|
|
185
|
+
)
|
|
186
|
+
return price
|
|
187
|
+
except AttributeError:
|
|
188
|
+
raise AttributeError(
|
|
189
|
+
f"Bars object must have 'adj_close' or 'close' prices"
|
|
190
|
+
)
|
|
177
191
|
|
|
178
192
|
def update_timeindex(self, event: MarketEvent):
|
|
179
193
|
"""
|
|
@@ -203,8 +217,8 @@ class Portfolio(object):
|
|
|
203
217
|
dh['Total'] = self.current_holdings['Cash']
|
|
204
218
|
for s in self.symbol_list:
|
|
205
219
|
# Approximation to the real value
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
price = self._get_price(s)
|
|
221
|
+
market_value = self.current_positions[s] * price
|
|
208
222
|
dh[s] = market_value
|
|
209
223
|
dh['Total'] += market_value
|
|
210
224
|
|
|
@@ -245,9 +259,7 @@ class Portfolio(object):
|
|
|
245
259
|
fill_dir = -1
|
|
246
260
|
|
|
247
261
|
# Update holdings list with new quantities
|
|
248
|
-
price = self.
|
|
249
|
-
fill.symbol, "adj_close"
|
|
250
|
-
)
|
|
262
|
+
price = self._get_price(fill.symbol)
|
|
251
263
|
cost = fill_dir * price * fill.quantity
|
|
252
264
|
self.current_holdings[fill.symbol] += cost
|
|
253
265
|
self.current_holdings['Commission'] += fill.commission
|
|
@@ -263,14 +275,16 @@ class Portfolio(object):
|
|
|
263
275
|
self.update_positions_from_fill(event)
|
|
264
276
|
self.update_holdings_from_fill(event)
|
|
265
277
|
|
|
266
|
-
def
|
|
278
|
+
def generate_order(self, signal: SignalEvent):
|
|
267
279
|
"""
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
position sizing considerations.
|
|
280
|
+
Check if the portfolio has enough cash to place an order
|
|
281
|
+
and generate an OrderEvent, else return None.
|
|
271
282
|
|
|
272
283
|
Args:
|
|
273
284
|
signal (SignalEvent): The tuple containing Signal information.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
OrderEvent: The OrderEvent to be executed.
|
|
274
288
|
"""
|
|
275
289
|
order = None
|
|
276
290
|
|
|
@@ -278,21 +292,33 @@ class Portfolio(object):
|
|
|
278
292
|
direction = signal.signal_type
|
|
279
293
|
quantity = signal.quantity
|
|
280
294
|
strength = signal.strength
|
|
281
|
-
price = signal.price
|
|
295
|
+
price = signal.price or self._get_price(symbol)
|
|
282
296
|
cur_quantity = self.current_positions[symbol]
|
|
297
|
+
cash = self.current_holdings['Cash']
|
|
283
298
|
|
|
284
|
-
|
|
299
|
+
if direction in ['LONG', 'SHORT', 'EXIT']:
|
|
300
|
+
order_type = 'MKT'
|
|
301
|
+
else:
|
|
302
|
+
order_type = direction
|
|
285
303
|
mkt_quantity = round(quantity * strength)
|
|
304
|
+
cost = mkt_quantity * price
|
|
305
|
+
|
|
306
|
+
if cash >= cost:
|
|
307
|
+
new_quantity = mkt_quantity
|
|
308
|
+
elif cash < cost and cash > 0:
|
|
309
|
+
new_quantity = round(cash // price)
|
|
310
|
+
else:
|
|
311
|
+
new_quantity = 0
|
|
286
312
|
|
|
287
|
-
if
|
|
288
|
-
order = OrderEvent(symbol, order_type,
|
|
289
|
-
if
|
|
290
|
-
order = OrderEvent(symbol, order_type,
|
|
313
|
+
if new_quantity > 0 and direction == 'LONG':
|
|
314
|
+
order = OrderEvent(symbol, order_type, new_quantity, 'BUY', price, direction)
|
|
315
|
+
if new_quantity > 0 and direction == 'SHORT':
|
|
316
|
+
order = OrderEvent(symbol, order_type, new_quantity, 'SELL', price, direction)
|
|
291
317
|
|
|
292
318
|
if direction == 'EXIT' and cur_quantity > 0:
|
|
293
|
-
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
|
|
319
|
+
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price, direction)
|
|
294
320
|
if direction == 'EXIT' and cur_quantity < 0:
|
|
295
|
-
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
|
|
321
|
+
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price, direction)
|
|
296
322
|
|
|
297
323
|
return order
|
|
298
324
|
|
|
@@ -302,7 +328,7 @@ class Portfolio(object):
|
|
|
302
328
|
based on the portfolio logic.
|
|
303
329
|
"""
|
|
304
330
|
if event.type == 'SIGNAL':
|
|
305
|
-
order_event = self.
|
|
331
|
+
order_event = self.generate_order(event)
|
|
306
332
|
self.events.put(order_event)
|
|
307
333
|
|
|
308
334
|
def create_equity_curve_dataframe(self):
|
bbstrader/btengine/strategy.py
CHANGED
|
@@ -7,6 +7,7 @@ from queue import Queue
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from bbstrader.config import config_logger
|
|
9
9
|
from bbstrader.btengine.event import SignalEvent
|
|
10
|
+
from bbstrader.btengine.event import FillEvent
|
|
10
11
|
from bbstrader.btengine.data import DataHandler
|
|
11
12
|
from bbstrader.metatrader.account import Account
|
|
12
13
|
from bbstrader.metatrader.rates import Rates
|
|
@@ -38,6 +39,8 @@ class Strategy(metaclass=ABCMeta):
|
|
|
38
39
|
|
|
39
40
|
The strategy hierarchy is relatively simple as it consists of an abstract
|
|
40
41
|
base class with a single pure virtual method for generating `SignalEvent` objects.
|
|
42
|
+
Other methods are provided to check for pending orders, update trades from fills,
|
|
43
|
+
and get updates from the portfolio.
|
|
41
44
|
"""
|
|
42
45
|
|
|
43
46
|
@abstractmethod
|
|
@@ -45,8 +48,9 @@ class Strategy(metaclass=ABCMeta):
|
|
|
45
48
|
raise NotImplementedError(
|
|
46
49
|
"Should implement calculate_signals()"
|
|
47
50
|
)
|
|
48
|
-
|
|
49
|
-
def
|
|
51
|
+
def check_pending_orders(self, *args, **kwargs): ...
|
|
52
|
+
def get_update_from_portfolio(self, *args, **kwargs): ...
|
|
53
|
+
def update_trades_from_fill(self, *args, **kwargs): ...
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
class MT5Strategy(Strategy):
|
|
@@ -74,17 +78,71 @@ class MT5Strategy(Strategy):
|
|
|
74
78
|
self.symbols = symbol_list
|
|
75
79
|
self.mode = mode
|
|
76
80
|
self.volume = kwargs.get("volume")
|
|
77
|
-
self.
|
|
78
|
-
|
|
81
|
+
self.max_trades = kwargs.get("max_trades",
|
|
82
|
+
{symbol: 1 for symbol in self.symbols})
|
|
83
|
+
self.logger = kwargs.get("logger")
|
|
84
|
+
self._initialize_portfolio()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
|
|
88
|
+
return self._orders
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def trades(self) -> Dict[str, Dict[str, int]]:
|
|
92
|
+
return self._trades
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def positions(self) -> Dict[str, Dict[str, int|float]]:
|
|
96
|
+
return self._positions
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def holdings(self) -> Dict[str, float]:
|
|
100
|
+
return self._holdings
|
|
79
101
|
|
|
80
|
-
def
|
|
81
|
-
self.positions: Dict[str, Dict[str, int]] = {}
|
|
82
|
-
self.orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
102
|
+
def _initialize_portfolio(self):
|
|
83
103
|
positions = ['LONG', 'SHORT']
|
|
84
104
|
orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
105
|
+
self._positions: Dict[str, Dict[str, int]] = {}
|
|
106
|
+
self._trades: Dict[str, Dict[str, int]] = {}
|
|
107
|
+
self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
85
108
|
for symbol in self.symbols:
|
|
86
|
-
self.
|
|
87
|
-
self.
|
|
109
|
+
self._positions[symbol] = {}
|
|
110
|
+
self._orders[symbol] = {}
|
|
111
|
+
self._trades[symbol] = {}
|
|
112
|
+
for position in positions:
|
|
113
|
+
self._trades[symbol][position] = 0
|
|
114
|
+
self._positions[symbol][position] = 0
|
|
115
|
+
for order in orders:
|
|
116
|
+
self._orders[symbol][order] = []
|
|
117
|
+
self._holdings = {s: 0.0 for s in self.symbols}
|
|
118
|
+
|
|
119
|
+
def get_update_from_portfolio(self, positions, holdings):
|
|
120
|
+
"""
|
|
121
|
+
Update the positions and holdings for the strategy from the portfolio.
|
|
122
|
+
|
|
123
|
+
Positions are the number of shares of a security that are owned in long or short.
|
|
124
|
+
Holdings are the value (postions * price) of the security that are owned in long or short.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
positions : The positions for the symbols in the strategy.
|
|
128
|
+
holdings : The holdings for the symbols in the strategy.
|
|
129
|
+
"""
|
|
130
|
+
for symbol in self.symbols:
|
|
131
|
+
if symbol in positions:
|
|
132
|
+
if positions[symbol] > 0:
|
|
133
|
+
self._positions[symbol]['LONG'] = positions[symbol]
|
|
134
|
+
elif positions[symbol] < 0:
|
|
135
|
+
self._positions[symbol]['SHORT'] = positions[symbol]
|
|
136
|
+
if symbol in holdings:
|
|
137
|
+
self._holdings[symbol] = holdings[symbol]
|
|
138
|
+
|
|
139
|
+
def update_trades_from_fill(self, event: FillEvent):
|
|
140
|
+
"""
|
|
141
|
+
This method updates the trades for the strategy based on the fill event.
|
|
142
|
+
It is used to keep track of the number of trades executed for each order.
|
|
143
|
+
"""
|
|
144
|
+
if event.type == 'FILL':
|
|
145
|
+
self._trades[event.symbol][event.order] += 1
|
|
88
146
|
|
|
89
147
|
def calculate_signals(self, *args, **kwargs
|
|
90
148
|
) -> Dict[str, Union[str, dict, None]] | None:
|
|
@@ -115,7 +173,7 @@ class MT5Strategy(Strategy):
|
|
|
115
173
|
"""
|
|
116
174
|
pass
|
|
117
175
|
|
|
118
|
-
def get_quantity(self, symbol) -> int:
|
|
176
|
+
def get_quantity(self, symbol, volume=None) -> int:
|
|
119
177
|
"""
|
|
120
178
|
Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
|
|
121
179
|
The quantity calculated can be used to evalute a strategy's performance for each symbol
|
|
@@ -127,11 +185,15 @@ class MT5Strategy(Strategy):
|
|
|
127
185
|
Returns:
|
|
128
186
|
qty : The quantity to buy or sell for the symbol.
|
|
129
187
|
"""
|
|
130
|
-
if self.volume is None:
|
|
188
|
+
if self.volume is None and volume is None:
|
|
131
189
|
raise ValueError("Volume must be provided for the method.")
|
|
132
190
|
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
try:
|
|
192
|
+
qty = math.ceil(self.volume or volume / current_price)
|
|
193
|
+
qty = max(qty, 1) / self.max_trades[symbol]
|
|
194
|
+
return max(math.ceil(qty), 1)
|
|
195
|
+
except Exception:
|
|
196
|
+
return 1
|
|
135
197
|
|
|
136
198
|
def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
|
|
137
199
|
"""
|
|
@@ -157,7 +219,7 @@ class MT5Strategy(Strategy):
|
|
|
157
219
|
self.logger.info(
|
|
158
220
|
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
|
|
159
221
|
|
|
160
|
-
def
|
|
222
|
+
def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
|
|
161
223
|
strength: float=1.0, dtime: datetime | pd.Timestamp=None):
|
|
162
224
|
"""
|
|
163
225
|
Open a long position
|
|
@@ -165,25 +227,22 @@ class MT5Strategy(Strategy):
|
|
|
165
227
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
166
228
|
"""
|
|
167
229
|
self._send_order(id, symbol, 'LONG', strength, price, quantity, dtime)
|
|
168
|
-
self.positions[symbol]['LONG'] += quantity
|
|
169
230
|
|
|
170
|
-
def
|
|
231
|
+
def sell_mkt(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
171
232
|
"""
|
|
172
233
|
Open a short position
|
|
173
234
|
|
|
174
235
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
175
236
|
"""
|
|
176
237
|
self._send_order(id, symbol, 'SHORT', strength, price, quantity, dtime)
|
|
177
|
-
self.positions[symbol]['SHORT'] += quantity
|
|
178
238
|
|
|
179
|
-
def
|
|
239
|
+
def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
180
240
|
"""
|
|
181
|
-
Close a position
|
|
241
|
+
Close a position or exit all positions
|
|
182
242
|
|
|
183
243
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
184
244
|
"""
|
|
185
245
|
self._send_order(id, symbol, 'EXIT', strength, price, quantity, dtime)
|
|
186
|
-
self.positions[symbol]['LONG'] -= quantity
|
|
187
246
|
|
|
188
247
|
def buy_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
189
248
|
"""
|
|
@@ -197,7 +256,7 @@ class MT5Strategy(Strategy):
|
|
|
197
256
|
"The buy_stop price must be greater than the current price.")
|
|
198
257
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
199
258
|
quantity=quantity, strength=strength, price=price)
|
|
200
|
-
self.
|
|
259
|
+
self._orders[symbol]['BSTP'].append(order)
|
|
201
260
|
|
|
202
261
|
def sell_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
203
262
|
"""
|
|
@@ -211,7 +270,7 @@ class MT5Strategy(Strategy):
|
|
|
211
270
|
"The sell_stop price must be less than the current price.")
|
|
212
271
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
213
272
|
quantity=quantity, strength=strength, price=price)
|
|
214
|
-
self.
|
|
273
|
+
self._orders[symbol]['SSTP'].append(order)
|
|
215
274
|
|
|
216
275
|
def buy_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
217
276
|
"""
|
|
@@ -225,7 +284,7 @@ class MT5Strategy(Strategy):
|
|
|
225
284
|
"The buy_limit price must be less than the current price.")
|
|
226
285
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
227
286
|
quantity=quantity, strength=strength, price=price)
|
|
228
|
-
self.
|
|
287
|
+
self._orders[symbol]['BLMT'].append(order)
|
|
229
288
|
|
|
230
289
|
def sell_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
231
290
|
"""
|
|
@@ -239,7 +298,7 @@ class MT5Strategy(Strategy):
|
|
|
239
298
|
"The sell_limit price must be greater than the current price.")
|
|
240
299
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
241
300
|
quantity=quantity, strength=strength, price=price)
|
|
242
|
-
self.
|
|
301
|
+
self._orders[symbol]['SLMT'].append(order)
|
|
243
302
|
|
|
244
303
|
def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
|
|
245
304
|
quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
|
|
@@ -257,7 +316,7 @@ class MT5Strategy(Strategy):
|
|
|
257
316
|
f"The stop-limit price {stoplimit} must be greater than the price {price}.")
|
|
258
317
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
259
318
|
quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
|
|
260
|
-
self.
|
|
319
|
+
self._orders[symbol]['BSTPLMT'].append(order)
|
|
261
320
|
|
|
262
321
|
def sell_stop_limit(self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None):
|
|
263
322
|
"""
|
|
@@ -274,7 +333,7 @@ class MT5Strategy(Strategy):
|
|
|
274
333
|
f"The stop-limit price {stoplimit} must be less than the price {price}.")
|
|
275
334
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
276
335
|
quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
|
|
277
|
-
self.
|
|
336
|
+
self._orders[symbol]['SSTPLMT'].append(order)
|
|
278
337
|
|
|
279
338
|
def check_pending_orders(self):
|
|
280
339
|
"""
|
|
@@ -282,54 +341,54 @@ class MT5Strategy(Strategy):
|
|
|
282
341
|
"""
|
|
283
342
|
for symbol in self.symbols:
|
|
284
343
|
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
285
|
-
for order in self.
|
|
344
|
+
for order in self._orders[symbol]['BLMT'].copy():
|
|
286
345
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
287
|
-
self.
|
|
346
|
+
self.buy_mkt(order.strategy_id, symbol,
|
|
288
347
|
order.price, order.quantity, dtime)
|
|
289
348
|
self.logger.info(
|
|
290
349
|
f"BUY LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
291
350
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
292
|
-
self.
|
|
293
|
-
for order in self.
|
|
351
|
+
self._orders[symbol]['BLMT'].remove(order)
|
|
352
|
+
for order in self._orders[symbol]['SLMT'].copy():
|
|
294
353
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
295
|
-
self.
|
|
354
|
+
self.sell_mkt(order.strategy_id, symbol,
|
|
296
355
|
order.price, order.quantity, dtime)
|
|
297
356
|
self.logger.info(
|
|
298
357
|
f"SELL LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
299
358
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
300
|
-
self.
|
|
301
|
-
for order in self.
|
|
359
|
+
self._orders[symbol]['SLMT'].remove(order)
|
|
360
|
+
for order in self._orders[symbol]['BSTP'].copy():
|
|
302
361
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
303
|
-
self.
|
|
362
|
+
self.buy_mkt(order.strategy_id, symbol,
|
|
304
363
|
order.price, order.quantity, dtime)
|
|
305
364
|
self.logger.info(
|
|
306
365
|
f"BUY STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
307
366
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
308
|
-
self.
|
|
309
|
-
for order in self.
|
|
367
|
+
self._orders[symbol]['BSTP'].remove(order)
|
|
368
|
+
for order in self._orders[symbol]['SSTP'].copy():
|
|
310
369
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
311
|
-
self.
|
|
370
|
+
self.sell_mkt(order.strategy_id, symbol,
|
|
312
371
|
order.price, order.quantity, dtime)
|
|
313
372
|
self.logger.info(
|
|
314
373
|
f"SELL STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
315
374
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
316
|
-
self.
|
|
317
|
-
for order in self.
|
|
375
|
+
self._orders[symbol]['SSTP'].remove(order)
|
|
376
|
+
for order in self._orders[symbol]['BSTPLMT'].copy():
|
|
318
377
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
319
378
|
self.buy_limit(order.strategy_id, symbol,
|
|
320
379
|
order.stoplimit, order.quantity, dtime)
|
|
321
380
|
self.logger.info(
|
|
322
381
|
f"BUY STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
323
382
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
324
|
-
self.
|
|
325
|
-
for order in self.
|
|
383
|
+
self._orders[symbol]['BSTPLMT'].remove(order)
|
|
384
|
+
for order in self._orders[symbol]['SSTPLMT'].copy():
|
|
326
385
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
327
386
|
self.sell_limit(order.strategy_id, symbol,
|
|
328
387
|
order.stoplimit, order.quantity, dtime)
|
|
329
388
|
self.logger.info(
|
|
330
389
|
f"SELL STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
331
390
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
332
|
-
self.
|
|
391
|
+
self._orders[symbol]['SSTPLMT'].remove(order)
|
|
333
392
|
|
|
334
393
|
def get_asset_values(self,
|
|
335
394
|
symbol_list: List[str],
|
bbstrader/metatrader/rates.py
CHANGED
|
@@ -178,8 +178,10 @@ class Rates(object):
|
|
|
178
178
|
return TIMEFRAMES[time_frame]
|
|
179
179
|
|
|
180
180
|
def _fetch_data(
|
|
181
|
-
self,
|
|
182
|
-
|
|
181
|
+
self,
|
|
182
|
+
start: Union[int, datetime, pd.Timestamp],
|
|
183
|
+
count: Union[int, datetime, pd.Timestamp],
|
|
184
|
+
lower_colnames=False, utc=False,
|
|
183
185
|
) -> Union[pd.DataFrame, None]:
|
|
184
186
|
"""Fetches data from MT5 and returns a DataFrame or None."""
|
|
185
187
|
try:
|
|
@@ -187,6 +189,13 @@ class Rates(object):
|
|
|
187
189
|
rates = Mt5.copy_rates_from_pos(
|
|
188
190
|
self.symbol, self.time_frame, start, count
|
|
189
191
|
)
|
|
192
|
+
elif (
|
|
193
|
+
isinstance(start, (datetime, pd.Timestamp)) and
|
|
194
|
+
isinstance(count, int)
|
|
195
|
+
):
|
|
196
|
+
rates = Mt5.copy_rates_from(
|
|
197
|
+
self.symbol, self.time_frame, start, count
|
|
198
|
+
)
|
|
190
199
|
elif (
|
|
191
200
|
isinstance(start, (datetime, pd.Timestamp)) and
|
|
192
201
|
isinstance(count, (datetime, pd.Timestamp))
|
|
@@ -320,6 +329,35 @@ class Rates(object):
|
|
|
320
329
|
return self._filter_data(df, fill_na=fill_na)
|
|
321
330
|
return df
|
|
322
331
|
|
|
332
|
+
def get_rates_from(self, date_from: datetime | pd.Timestamp, count: int=MAX_BARS,
|
|
333
|
+
filter=False, fill_na=False, lower_colnames=False, utc=False) -> Union[pd.DataFrame, None]:
|
|
334
|
+
"""
|
|
335
|
+
Retrieves historical data within a specified date range.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
date_from : Starting date for data retrieval.
|
|
339
|
+
The data will be retrieved from this date going to the past.
|
|
340
|
+
|
|
341
|
+
count : Number of bars to retrieve.
|
|
342
|
+
|
|
343
|
+
filter : See `Rates.get_historical_data` for more details.
|
|
344
|
+
fill_na : See `Rates.get_historical_data` for more details.
|
|
345
|
+
lower_colnames : If True, the column names will be converted to lowercase.
|
|
346
|
+
utc (bool, optional): If True, the data will be in UTC timezone.
|
|
347
|
+
Defaults to False.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Union[pd.DataFrame, None]: A DataFrame containing historical
|
|
351
|
+
data if successful, otherwise None.
|
|
352
|
+
"""
|
|
353
|
+
utc = self._check_filter(filter, utc)
|
|
354
|
+
df = self._fetch_data(date_from, count, lower_colnames=lower_colnames, utc=utc)
|
|
355
|
+
if df is None:
|
|
356
|
+
return None
|
|
357
|
+
if filter:
|
|
358
|
+
return self._filter_data(df, fill_na=fill_na)
|
|
359
|
+
return df
|
|
360
|
+
|
|
323
361
|
@property
|
|
324
362
|
def open(self):
|
|
325
363
|
return self.__data['Open']
|
|
@@ -459,4 +497,14 @@ def get_data_from_pos(symbol, time_frame, start_pos=0, fill_na=False,
|
|
|
459
497
|
rates = Rates(symbol, time_frame, start_pos, count, session_duration)
|
|
460
498
|
data = rates.get_rates_from_pos(filter=filter, fill_na=fill_na,
|
|
461
499
|
lower_colnames=lower_colnames, utc=utc)
|
|
500
|
+
return data
|
|
501
|
+
|
|
502
|
+
def get_data_from_date(symbol, time_frame, date_from, count=MAX_BARS, fill_na=False,
|
|
503
|
+
lower_colnames=False, utc=False, filter=False):
|
|
504
|
+
"""Get historical data from a specific date.
|
|
505
|
+
See `Rates.get_rates_from` for more details.
|
|
506
|
+
"""
|
|
507
|
+
rates = Rates(symbol, time_frame)
|
|
508
|
+
data = rates.get_rates_from(date_from, count, filter=filter, fill_na=fill_na,
|
|
509
|
+
lower_colnames=lower_colnames, utc=utc)
|
|
462
510
|
return data
|
bbstrader/metatrader/trade.py
CHANGED
|
@@ -838,14 +838,16 @@ class Trade(RiskManagement):
|
|
|
838
838
|
elif account and id is not None:
|
|
839
839
|
# All open positions for a specific strategy or expert no matter the symbol
|
|
840
840
|
positions = self.get_positions()
|
|
841
|
-
|
|
841
|
+
if positions is not None:
|
|
842
|
+
positions = [position for position in positions if position.magic == id]
|
|
842
843
|
elif not account and id is None:
|
|
843
844
|
# All open positions for the current symbol no matter the strategy or expert
|
|
844
845
|
positions = self.get_positions(symbol=self.symbol)
|
|
845
846
|
elif not account and id is not None:
|
|
846
847
|
# All open positions for the current symbol and a specific strategy or expert
|
|
847
848
|
positions = self.get_positions(symbol=self.symbol)
|
|
848
|
-
|
|
849
|
+
if positions is not None:
|
|
850
|
+
positions = [position for position in positions if position.magic == id]
|
|
849
851
|
profit = 0.0
|
|
850
852
|
balance = self.get_account_info().balance
|
|
851
853
|
target = round((balance * self.target)/100, 2)
|
|
@@ -1026,8 +1028,9 @@ class Trade(RiskManagement):
|
|
|
1026
1028
|
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1027
1029
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1028
1030
|
msg = trade_retcode_message(result.retcode)
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
+
if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
1032
|
+
self.logger.error(
|
|
1033
|
+
f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
1031
1034
|
tries = 0
|
|
1032
1035
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
|
|
1033
1036
|
if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
|
bbstrader/models/optimization.py
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from pypfopt import risk_models
|
|
4
|
+
from pypfopt import expected_returns
|
|
5
|
+
from pypfopt.efficient_frontier import EfficientFrontier
|
|
6
|
+
from pypfopt.hierarchical_portfolio import HRPOpt
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
def markowitz_weights(prices=None, freq=252):
|
|
10
|
+
"""
|
|
11
|
+
Calculates optimal portfolio weights using Markowitz's mean-variance optimization (Max Sharpe Ratio) with multiple solvers.
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
----------
|
|
15
|
+
prices : pd.DataFrame, optional
|
|
16
|
+
Price data for assets, where rows represent time periods and columns represent assets.
|
|
17
|
+
freq : int, optional
|
|
18
|
+
Frequency of the data, such as 252 for daily returns in a year (default is 252).
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
-------
|
|
22
|
+
dict
|
|
23
|
+
Dictionary containing the optimal asset weights for maximizing the Sharpe ratio, normalized to sum to 1.
|
|
24
|
+
|
|
25
|
+
Notes:
|
|
26
|
+
-----
|
|
27
|
+
This function attempts to maximize the Sharpe ratio by iterating through various solvers ('SCS', 'ECOS', 'OSQP')
|
|
28
|
+
from the PyPortfolioOpt library. If a solver fails, it proceeds to the next one. If none succeed, an error message
|
|
29
|
+
is printed for each solver that fails.
|
|
30
|
+
|
|
31
|
+
This function is useful for portfolio with a small number of assets, as it may not scale well for large portfolios.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
------
|
|
35
|
+
Exception
|
|
36
|
+
If all solvers fail, each will print an exception error message during runtime.
|
|
37
|
+
"""
|
|
38
|
+
returns = expected_returns.mean_historical_return(prices, frequency=freq)
|
|
39
|
+
cov = risk_models.sample_cov(prices, frequency=freq)
|
|
40
|
+
|
|
41
|
+
# Try different solvers to maximize Sharpe ratio
|
|
42
|
+
for solver in ['SCS', 'ECOS', 'OSQP']:
|
|
43
|
+
ef = EfficientFrontier(expected_returns=returns,
|
|
44
|
+
cov_matrix=cov,
|
|
45
|
+
weight_bounds=(0, 1),
|
|
46
|
+
solver=solver)
|
|
47
|
+
try:
|
|
48
|
+
weights = ef.max_sharpe()
|
|
49
|
+
return ef.clean_weights()
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"Solver {solver} failed with error: {e}")
|
|
52
|
+
|
|
53
|
+
def hierarchical_risk_parity(prices=None, returns=None, freq=252):
|
|
54
|
+
"""
|
|
55
|
+
Computes asset weights using Hierarchical Risk Parity (HRP) for risk-averse portfolio allocation.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
----------
|
|
59
|
+
prices : pd.DataFrame, optional
|
|
60
|
+
Price data for assets; if provided, daily returns will be calculated.
|
|
61
|
+
returns : pd.DataFrame, optional
|
|
62
|
+
Daily returns for assets. One of `prices` or `returns` must be provided.
|
|
63
|
+
freq : int, optional
|
|
64
|
+
Number of days to consider in calculating portfolio weights (default is 252).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
-------
|
|
68
|
+
dict
|
|
69
|
+
Optimized asset weights using the HRP method, with asset weights summing to 1.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
------
|
|
73
|
+
ValueError
|
|
74
|
+
If neither `prices` nor `returns` are provided.
|
|
75
|
+
|
|
76
|
+
Notes:
|
|
77
|
+
-----
|
|
78
|
+
Hierarchical Risk Parity is particularly useful for portfolios with a large number of assets,
|
|
79
|
+
as it mitigates issues of multicollinearity and estimation errors in covariance matrices by
|
|
80
|
+
using hierarchical clustering.
|
|
81
|
+
"""
|
|
82
|
+
warnings.filterwarnings("ignore")
|
|
83
|
+
if returns is None and prices is None:
|
|
84
|
+
raise ValueError("Either prices or returns must be provided")
|
|
85
|
+
if returns is None:
|
|
86
|
+
returns = prices.pct_change().dropna()
|
|
87
|
+
# Remove duplicate columns and index
|
|
88
|
+
returns = returns.loc[:, ~returns.columns.duplicated()]
|
|
89
|
+
returns = returns.loc[~returns.index.duplicated(keep='first')]
|
|
90
|
+
hrp = HRPOpt(returns=returns.iloc[-freq:])
|
|
91
|
+
return hrp.optimize()
|
|
92
|
+
|
|
93
|
+
def equal_weighted(prices=None, returns=None, round_digits=5):
|
|
94
|
+
"""
|
|
95
|
+
Generates an equal-weighted portfolio by assigning an equal proportion to each asset.
|
|
96
|
+
|
|
97
|
+
Parameters:
|
|
98
|
+
----------
|
|
99
|
+
prices : pd.DataFrame, optional
|
|
100
|
+
Price data for assets, where each column represents an asset.
|
|
101
|
+
returns : pd.DataFrame, optional
|
|
102
|
+
Return data for assets. One of `prices` or `returns` must be provided.
|
|
103
|
+
round_digits : int, optional
|
|
104
|
+
Number of decimal places to round each weight to (default is 5).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
-------
|
|
108
|
+
dict
|
|
109
|
+
Dictionary with equal weights assigned to each asset, summing to 1.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
------
|
|
113
|
+
ValueError
|
|
114
|
+
If neither `prices` nor `returns` are provided.
|
|
115
|
+
|
|
116
|
+
Notes:
|
|
117
|
+
-----
|
|
118
|
+
Equal weighting is a simple allocation method that assumes equal importance across all assets,
|
|
119
|
+
useful as a baseline model and when no strong views exist on asset return expectations or risk.
|
|
120
|
+
"""
|
|
121
|
+
if returns is None and prices is None:
|
|
122
|
+
raise ValueError("Either prices or returns must be provided")
|
|
123
|
+
if returns is None:
|
|
124
|
+
n = len(prices.columns)
|
|
125
|
+
columns = prices.columns
|
|
126
|
+
else:
|
|
127
|
+
n = len(returns.columns)
|
|
128
|
+
columns = returns.columns
|
|
129
|
+
return {col: round(1/n, round_digits) for col in columns}
|
|
130
|
+
|
|
131
|
+
def optimized_weights(prices=None, returns=None, freq=252, method='markowitz'):
|
|
132
|
+
"""
|
|
133
|
+
Selects an optimization method to calculate portfolio weights based on user preference.
|
|
134
|
+
|
|
135
|
+
Parameters:
|
|
136
|
+
----------
|
|
137
|
+
prices : pd.DataFrame, optional
|
|
138
|
+
Price data for assets, required for certain methods.
|
|
139
|
+
returns : pd.DataFrame, optional
|
|
140
|
+
Returns data for assets, an alternative input for certain methods.
|
|
141
|
+
freq : int, optional
|
|
142
|
+
Number of days for calculating portfolio weights, such as 252 for a year's worth of daily returns (default is 252).
|
|
143
|
+
method : str, optional
|
|
144
|
+
Optimization method to use ('markowitz', 'hrp', or 'equal') (default is 'markowitz').
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
-------
|
|
148
|
+
dict
|
|
149
|
+
Dictionary containing optimized asset weights based on the chosen method.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
------
|
|
153
|
+
ValueError
|
|
154
|
+
If an unknown optimization method is specified.
|
|
155
|
+
|
|
156
|
+
Notes:
|
|
157
|
+
-----
|
|
158
|
+
This function integrates different optimization methods:
|
|
159
|
+
- 'markowitz': mean-variance optimization with max Sharpe ratio
|
|
160
|
+
- 'hrp': Hierarchical Risk Parity, for risk-based clustering of assets
|
|
161
|
+
- 'equal': Equal weighting across all assets
|
|
162
|
+
"""
|
|
163
|
+
if method == 'markowitz':
|
|
164
|
+
return markowitz_weights(prices=prices, freq=freq)
|
|
165
|
+
elif method == 'hrp':
|
|
166
|
+
return hierarchical_risk_parity(prices=prices, returns=returns, freq=freq)
|
|
167
|
+
elif method == 'equal':
|
|
168
|
+
return equal_weighted(prices=prices, returns=returns)
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Unknown method: {method}")
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import seaborn as sns
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from sklearn.decomposition import PCA
|
|
6
|
+
from sklearn.preprocessing import scale
|
|
7
|
+
from bbstrader.models.optimization import (
|
|
8
|
+
markowitz_weights,
|
|
9
|
+
hierarchical_risk_parity,
|
|
10
|
+
equal_weighted
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EigenPortfolios(object):
|
|
15
|
+
"""
|
|
16
|
+
The `EigenPortfolios` class applies Principal Component Analysis (PCA) to a covariance matrix of normalized asset returns
|
|
17
|
+
to derive portfolios (eigenportfolios) that capture distinct risk factors in the asset returns. Each eigenportfolio
|
|
18
|
+
represents a principal component of the return covariance matrix, ordered by the magnitude of its eigenvalue. These
|
|
19
|
+
portfolios capture most of the variance in asset returns and are mutually uncorrelated.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.returns = None
|
|
24
|
+
self.n_portfolios = None
|
|
25
|
+
self._portfolios = None
|
|
26
|
+
self._fit_called = False
|
|
27
|
+
|
|
28
|
+
def get_portfolios(self) -> pd.DataFrame:
|
|
29
|
+
"""
|
|
30
|
+
Returns the computed eigenportfolios (weights of assets in each portfolio).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
-------
|
|
34
|
+
pd.DataFrame
|
|
35
|
+
DataFrame containing eigenportfolio weights for each asset.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
------
|
|
39
|
+
ValueError
|
|
40
|
+
If `fit()` has not been called before retrieving portfolios.
|
|
41
|
+
"""
|
|
42
|
+
if not self._fit_called:
|
|
43
|
+
raise ValueError("fit() must be called first")
|
|
44
|
+
return self._portfolios
|
|
45
|
+
|
|
46
|
+
def fit(self, returns: pd.DataFrame, n_portfolios: int=4) -> pd.DataFrame:
|
|
47
|
+
"""
|
|
48
|
+
Computes the eigenportfolios based on PCA of the asset returns' covariance matrix.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
----------
|
|
52
|
+
returns : pd.DataFrame
|
|
53
|
+
Historical returns of assets to be used for PCA.
|
|
54
|
+
n_portfolios : int, optional
|
|
55
|
+
Number of eigenportfolios to compute (default is 4).
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
-------
|
|
59
|
+
pd.DataFrame
|
|
60
|
+
DataFrame containing normalized weights for each eigenportfolio.
|
|
61
|
+
|
|
62
|
+
Notes:
|
|
63
|
+
-----
|
|
64
|
+
This method performs winsorization and normalization on returns to reduce the impact of outliers
|
|
65
|
+
and achieve zero mean and unit variance. It uses the first `n_portfolios` principal components
|
|
66
|
+
as portfolio weights.
|
|
67
|
+
"""
|
|
68
|
+
# Winsorize and normalize the returns
|
|
69
|
+
normed_returns = scale(returns
|
|
70
|
+
.clip(lower=returns.quantile(q=.025),
|
|
71
|
+
upper=returns.quantile(q=.975),
|
|
72
|
+
axis=1)
|
|
73
|
+
.apply(lambda x: x.sub(x.mean()).div(x.std())))
|
|
74
|
+
returns = returns.dropna(thresh=int(normed_returns.shape[0] * .95), axis=1)
|
|
75
|
+
returns = returns.dropna(thresh=int(normed_returns.shape[1] * .95))
|
|
76
|
+
|
|
77
|
+
cov = returns.cov()
|
|
78
|
+
cov.columns = cov.columns.astype(str)
|
|
79
|
+
pca = PCA()
|
|
80
|
+
pca.fit(cov)
|
|
81
|
+
|
|
82
|
+
top_portfolios = pd.DataFrame(pca.components_[:n_portfolios], columns=cov.columns)
|
|
83
|
+
eigen_portfolios = top_portfolios.div(top_portfolios.sum(axis=1), axis=0)
|
|
84
|
+
eigen_portfolios.index = [f"Portfolio {i}" for i in range(1, n_portfolios + 1)]
|
|
85
|
+
self._portfolios = eigen_portfolios
|
|
86
|
+
self.returns = returns
|
|
87
|
+
self.n_portfolios = n_portfolios
|
|
88
|
+
self._fit_called = True
|
|
89
|
+
|
|
90
|
+
def plot_weights(self):
|
|
91
|
+
"""
|
|
92
|
+
Plots the weights of each asset in each eigenportfolio as bar charts.
|
|
93
|
+
|
|
94
|
+
Notes:
|
|
95
|
+
-----
|
|
96
|
+
Each subplot represents one eigenportfolio, showing the contribution of each asset.
|
|
97
|
+
"""
|
|
98
|
+
eigen_portfolios = self.get_portfolios()
|
|
99
|
+
n_cols = 2
|
|
100
|
+
n_rows = (self.n_portfolios + 1) // n_cols
|
|
101
|
+
figsize = (n_cols * 10, n_rows * 5)
|
|
102
|
+
axes = eigen_portfolios.T.plot.bar(subplots=True,
|
|
103
|
+
layout=(n_rows, n_cols),
|
|
104
|
+
figsize=figsize,
|
|
105
|
+
legend=False)
|
|
106
|
+
for ax in axes.flatten():
|
|
107
|
+
ax.set_ylabel('Portfolio Weight')
|
|
108
|
+
ax.set_xlabel('')
|
|
109
|
+
|
|
110
|
+
sns.despine()
|
|
111
|
+
plt.tight_layout()
|
|
112
|
+
plt.show()
|
|
113
|
+
|
|
114
|
+
def plot_performance(self):
|
|
115
|
+
"""
|
|
116
|
+
Plots the cumulative returns of each eigenportfolio over time.
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
-----
|
|
120
|
+
This method calculates the historical cumulative performance of each eigenportfolio
|
|
121
|
+
by weighting asset returns according to eigenportfolio weights.
|
|
122
|
+
"""
|
|
123
|
+
eigen_portfolios = self.get_portfolios()
|
|
124
|
+
returns = self.returns.copy()
|
|
125
|
+
|
|
126
|
+
n_cols = 2
|
|
127
|
+
n_rows = (self.n_portfolios + 1 + n_cols - 1) // n_cols
|
|
128
|
+
figsize = (n_cols * 10, n_rows * 5)
|
|
129
|
+
fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols,
|
|
130
|
+
figsize=figsize, sharex=True)
|
|
131
|
+
axes = axes.flatten()
|
|
132
|
+
returns.mean(1).add(1).cumprod().sub(1).plot(title='The Market', ax=axes[0])
|
|
133
|
+
|
|
134
|
+
for i in range(self.n_portfolios):
|
|
135
|
+
rc = returns.mul(eigen_portfolios.iloc[i]).sum(1).add(1).cumprod().sub(1)
|
|
136
|
+
rc.plot(title=f'Portfolio {i+1}', ax=axes[i + 1], lw=1, rot=0)
|
|
137
|
+
|
|
138
|
+
for j in range(self.n_portfolios + 1, len(axes)):
|
|
139
|
+
fig.delaxes(axes[j])
|
|
140
|
+
|
|
141
|
+
for i in range(self.n_portfolios + 1):
|
|
142
|
+
axes[i].set_xlabel('')
|
|
143
|
+
|
|
144
|
+
sns.despine()
|
|
145
|
+
fig.tight_layout()
|
|
146
|
+
plt.show()
|
|
147
|
+
|
|
148
|
+
def optimize(self, portfolio: int = 1, optimizer: str = 'hrp', prices=None, freq=252, plot=True):
|
|
149
|
+
"""
|
|
150
|
+
Optimizes the chosen eigenportfolio based on a specified optimization method.
|
|
151
|
+
|
|
152
|
+
Parameters:
|
|
153
|
+
----------
|
|
154
|
+
portfolio : int, optional
|
|
155
|
+
Index of the eigenportfolio to optimize (default is 1).
|
|
156
|
+
optimizer : str, optional
|
|
157
|
+
Optimization method: 'markowitz', 'hrp' (Hierarchical Risk Parity), or 'equal' (default is 'hrp').
|
|
158
|
+
prices : pd.DataFrame, optional
|
|
159
|
+
Asset prices used for Markowitz optimization (required if optimizer is 'markowitz').
|
|
160
|
+
freq : int, optional
|
|
161
|
+
Frequency of returns (e.g., 252 for daily returns).
|
|
162
|
+
plot : bool, optional
|
|
163
|
+
Whether to plot the performance of the optimized portfolio (default is True).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
-------
|
|
167
|
+
dict
|
|
168
|
+
Dictionary of optimized asset weights.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
------
|
|
172
|
+
ValueError
|
|
173
|
+
If an unknown optimizer is specified, or if prices are not provided when using Markowitz optimization.
|
|
174
|
+
|
|
175
|
+
Notes:
|
|
176
|
+
-----
|
|
177
|
+
The optimization method varies based on risk-return assumptions, with options for traditional Markowitz optimization,
|
|
178
|
+
Hierarchical Risk Parity, or equal weighting.
|
|
179
|
+
"""
|
|
180
|
+
portfolio = self.get_portfolios().iloc[portfolio - 1]
|
|
181
|
+
returns = self.returns.loc[:, portfolio.index]
|
|
182
|
+
returns = returns.loc[:, ~returns.columns.duplicated()]
|
|
183
|
+
returns = returns.loc[~returns.index.duplicated(keep='first')]
|
|
184
|
+
if optimizer == 'markowitz':
|
|
185
|
+
if prices is None:
|
|
186
|
+
raise ValueError("prices must be provided for markowitz optimization")
|
|
187
|
+
prices = prices.loc[:, returns.columns]
|
|
188
|
+
weights = markowitz_weights(prices=prices, freq=freq)
|
|
189
|
+
elif optimizer == 'hrp':
|
|
190
|
+
weights = hierarchical_risk_parity(returns=returns, freq=freq)
|
|
191
|
+
elif optimizer == 'equal':
|
|
192
|
+
weights = equal_weighted(returns=returns)
|
|
193
|
+
else:
|
|
194
|
+
raise ValueError(f"Unknown optimizer: {optimizer}")
|
|
195
|
+
if plot:
|
|
196
|
+
# plot the optimized potfolio performance
|
|
197
|
+
returns = returns.filter(weights.keys())
|
|
198
|
+
rc = returns.mul(weights).sum(1).add(1).cumprod().sub(1)
|
|
199
|
+
rc.plot(title=f'Optimized {portfolio.name}', lw=1, rot=0)
|
|
200
|
+
sns.despine()
|
|
201
|
+
plt.show()
|
|
202
|
+
return weights
|
bbstrader/trading/execution.py
CHANGED
|
@@ -90,7 +90,7 @@ def _mt5_execution(
|
|
|
90
90
|
bot_token = kwargs.get('bot_token')
|
|
91
91
|
chat_id = kwargs.get('chat_id')
|
|
92
92
|
|
|
93
|
-
def _send_notification(
|
|
93
|
+
def _send_notification(signal):
|
|
94
94
|
send_message(message=signal, notify_me=notify,
|
|
95
95
|
telegram=telegram, token=bot_token, chat_id=chat_id)
|
|
96
96
|
|
|
@@ -104,8 +104,6 @@ def _mt5_execution(
|
|
|
104
104
|
if not mm:
|
|
105
105
|
return
|
|
106
106
|
if buys is not None or sells is not None:
|
|
107
|
-
logger.info(
|
|
108
|
-
f"Checking for Break even, SYMBOL={symbol}...STRATEGY={STRATEGY}")
|
|
109
107
|
trades_instances[symbol].break_even(
|
|
110
108
|
mm=mm, trail=trail, stop_trail=stop_trail,
|
|
111
109
|
trail_after_points=trail_after_points, be_plus_points=be_plus_points)
|
|
@@ -119,10 +117,10 @@ def _mt5_execution(
|
|
|
119
117
|
check_mt5_connection()
|
|
120
118
|
strategy: MT5Strategy = strategy_cls(symbol_list=symbols, mode='live', **kwargs)
|
|
121
119
|
except Exception as e:
|
|
122
|
-
logger.error(f"
|
|
120
|
+
logger.error(f"Initializing strategy, {e}, STRATEGY={STRATEGY}")
|
|
123
121
|
return
|
|
124
122
|
logger.info(
|
|
125
|
-
f'Running {STRATEGY} Strategy
|
|
123
|
+
f'Running {STRATEGY} Strategy in {time_frame} Interval ...')
|
|
126
124
|
|
|
127
125
|
while True:
|
|
128
126
|
try:
|
|
@@ -132,7 +130,9 @@ def _mt5_execution(
|
|
|
132
130
|
time.sleep(0.5)
|
|
133
131
|
positions_orders = {}
|
|
134
132
|
for type in POSITIONS_TYPES + ORDERS_TYPES:
|
|
133
|
+
positions_orders[type] = {}
|
|
135
134
|
for symbol in symbols:
|
|
135
|
+
positions_orders[type][symbol] = None
|
|
136
136
|
func = getattr(trades_instances[symbol], f"get_current_{type}")
|
|
137
137
|
positions_orders[type][symbol] = func()
|
|
138
138
|
buys = positions_orders['buys']
|
|
@@ -154,7 +154,7 @@ def _mt5_execution(
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
except Exception as e:
|
|
157
|
-
logger.error(f"{e}, STRATEGY={STRATEGY}")
|
|
157
|
+
logger.error(f"Handling Positions and Orders, {e}, STRATEGY={STRATEGY}")
|
|
158
158
|
continue
|
|
159
159
|
time.sleep(0.5)
|
|
160
160
|
try:
|
|
@@ -183,7 +183,8 @@ def _mt5_execution(
|
|
|
183
183
|
signal = 'SMKT' if signal == 'SHORT' else signal
|
|
184
184
|
info = f"SIGNAL = {signal}, SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
|
|
185
185
|
msg = f"Sending {signal} Order ... SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
|
|
186
|
-
|
|
186
|
+
if signal not in EXIT_SIGNAL_ACTIONS:
|
|
187
|
+
logger.info(info)
|
|
187
188
|
if signal in EXIT_SIGNAL_ACTIONS:
|
|
188
189
|
for exit_signal, actions in EXIT_SIGNAL_ACTIONS.items():
|
|
189
190
|
for position_type, order_type in actions.items():
|
|
@@ -235,13 +236,15 @@ def _mt5_execution(
|
|
|
235
236
|
elif signal in SELLS and short_market[symbol]:
|
|
236
237
|
logger.info(riskmsg)
|
|
237
238
|
check(buys[symbol], sells[symbol], symbol)
|
|
239
|
+
else:
|
|
240
|
+
check(buys[symbol], sells[symbol], symbol)
|
|
238
241
|
else:
|
|
239
242
|
logger.info(
|
|
240
243
|
f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}")
|
|
241
244
|
check(buys[symbol], sells[symbol], symbol)
|
|
242
245
|
|
|
243
246
|
except Exception as e:
|
|
244
|
-
logger.error(f"{e}, SYMBOL={symbol}, STRATEGY={STRATEGY}")
|
|
247
|
+
logger.error(f"Handling Signals {e}, SYMBOL={symbol}, STRATEGY={STRATEGY}")
|
|
245
248
|
continue
|
|
246
249
|
time.sleep((60 * iter_time) - 1.0)
|
|
247
250
|
if iter_time == 1:
|
|
@@ -253,7 +256,6 @@ def _mt5_execution(
|
|
|
253
256
|
f"iter_time must be a multiple of the {time_frame} !!!"
|
|
254
257
|
f"(e.g; if time_frame is 15m, iter_time must be 1.5, 3, 3, 15 etc)"
|
|
255
258
|
)
|
|
256
|
-
print()
|
|
257
259
|
try:
|
|
258
260
|
FRIDAY = 'friday'
|
|
259
261
|
check_mt5_connection()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.92
|
|
4
4
|
Summary: Simplified Investment & Trading Toolkit
|
|
5
5
|
Home-page: https://github.com/bbalouki/bbstrader
|
|
6
6
|
Download-URL: https://pypi.org/project/bbstrader/
|
|
@@ -46,6 +46,7 @@ Requires-Dist: tqdm
|
|
|
46
46
|
Requires-Dist: scikit-learn
|
|
47
47
|
Requires-Dist: notify-py
|
|
48
48
|
Requires-Dist: python-telegram-bot
|
|
49
|
+
Requires-Dist: pyportfolioopt
|
|
49
50
|
Provides-Extra: mt5
|
|
50
51
|
Requires-Dist: MetaTrader5 ; extra == 'mt5'
|
|
51
52
|
|
|
@@ -2,30 +2,31 @@ bbstrader/__ini__.py,sha256=rCTy-3g2RlDAgIZ7cSET9-I74MwuCXpp-xGVTFS8NNc,482
|
|
|
2
2
|
bbstrader/config.py,sha256=_AD_Cd-w5zyabm1CBPNGhzcZuSjThB7jyzTcjbrIlUQ,3618
|
|
3
3
|
bbstrader/tseries.py,sha256=qJKLxHnPOjB7dXon-ITK7vU1fAuvl8evzET6lSSnijQ,53572
|
|
4
4
|
bbstrader/btengine/__init__.py,sha256=OaXZTjgDwqWrjPq-CNE4kJkmriKXt9t5pIghW1MDTeo,2911
|
|
5
|
-
bbstrader/btengine/backtest.py,sha256=
|
|
6
|
-
bbstrader/btengine/data.py,sha256=
|
|
7
|
-
bbstrader/btengine/event.py,sha256=
|
|
8
|
-
bbstrader/btengine/execution.py,sha256=
|
|
5
|
+
bbstrader/btengine/backtest.py,sha256=bgQNiS_kb1zWyX_8_OIDGTBlHGC5pw7ZMIQ5csYDahA,14115
|
|
6
|
+
bbstrader/btengine/data.py,sha256=A6jUqDnjl-w1OSzbLLPfS1WfJ8Se25AqigJs9pbe0wc,17966
|
|
7
|
+
bbstrader/btengine/event.py,sha256=zF_ST4tcjV5uJJVV1IbRXQgCLbca2R2fmE7A2MaIno4,8748
|
|
8
|
+
bbstrader/btengine/execution.py,sha256=i-vI9LGqVtEIKfH_T5Airv-gI4t1X75CfuFIuQdogkA,10187
|
|
9
9
|
bbstrader/btengine/performance.py,sha256=bKwj1_CSygvggLKTXPASp2eWhDdwyCf06ayUaXwdh4E,10655
|
|
10
|
-
bbstrader/btengine/portfolio.py,sha256=
|
|
11
|
-
bbstrader/btengine/strategy.py,sha256=
|
|
10
|
+
bbstrader/btengine/portfolio.py,sha256=9Jw0UA2gPu-YkUNenoMSHt8t-axtbl6veWXgcRMTQ14,16074
|
|
11
|
+
bbstrader/btengine/strategy.py,sha256=w5vwlpJNIVjslaHprWDr-3j-JJPiO1EvqeyrJmorByE,25721
|
|
12
12
|
bbstrader/metatrader/__init__.py,sha256=OLVOB_EieEb1P72I8V4Vem8kQWJ__D_L3c_wfwqY-9k,211
|
|
13
13
|
bbstrader/metatrader/account.py,sha256=hVH83vnAdfMOzUsF9PiWelqxa7HaLSTpCVlUEePnSZg,53912
|
|
14
|
-
bbstrader/metatrader/rates.py,sha256=
|
|
14
|
+
bbstrader/metatrader/rates.py,sha256=1dJHbVqoT41m3EhF0wRe7dSGe5Kf3o5Maskkw-i5qsQ,20810
|
|
15
15
|
bbstrader/metatrader/risk.py,sha256=8FcLY8pgV8_rxAcjx179sdqaMu66wl-fDFPZvdihfUw,25953
|
|
16
|
-
bbstrader/metatrader/trade.py,sha256=
|
|
16
|
+
bbstrader/metatrader/trade.py,sha256=uigDah9n_rVJiwSslTAArLP94sde1dxYyGyRVIPPgb4,70210
|
|
17
17
|
bbstrader/metatrader/utils.py,sha256=BTaZun4DKWpCxBBzY0SLQqqz7n_7F_R1F59APfyaa3E,17666
|
|
18
18
|
bbstrader/models/__init__.py,sha256=6tAj9V9vgwesgPVMKznwRB3k8-Ec8Q73Di5p2UO0qlA,274
|
|
19
19
|
bbstrader/models/factors.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
bbstrader/models/ml.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
bbstrader/models/optimization.py,sha256=
|
|
21
|
+
bbstrader/models/optimization.py,sha256=0ZOCinveMCSxpL4gTBO5lbZ6jb1HXp0CeIjO-_fNGro,6521
|
|
22
|
+
bbstrader/models/portfolios.py,sha256=TyLcYwxW86t1qH6YS5xBaiKB9Owezq3ovffjR0Othlw,8184
|
|
22
23
|
bbstrader/models/risk.py,sha256=Pm_WoGI-vtPW75fwo_7ptF2Br-xQYBwrAAOIgqDQmy8,15120
|
|
23
24
|
bbstrader/trading/__init__.py,sha256=3CCzV5rQbH8NthjDJhD0_2FABvpiCmkeC9cVeoW7bi4,438
|
|
24
|
-
bbstrader/trading/execution.py,sha256=
|
|
25
|
+
bbstrader/trading/execution.py,sha256=Ze_YFrdgU9mRZsS4J08lZNE77HBwxrun5rdqHkS4dzE,25348
|
|
25
26
|
bbstrader/trading/scripts.py,sha256=rQmnG_4F_MuUEc96RXpAQT4kXrC-FkscsgHKgDAR_-Y,1902
|
|
26
27
|
bbstrader/trading/strategies.py,sha256=ztKNL4Nmlb-4N8_cq0OJyn3E2cRcdKdKu3FeTbZrHsU,36402
|
|
27
|
-
bbstrader-0.1.
|
|
28
|
-
bbstrader-0.1.
|
|
29
|
-
bbstrader-0.1.
|
|
30
|
-
bbstrader-0.1.
|
|
31
|
-
bbstrader-0.1.
|
|
28
|
+
bbstrader-0.1.92.dist-info/LICENSE,sha256=1EudjwwP2oTJy8Vh0e-Kzv8VZZU95y-t6c3DYhR51uc,1115
|
|
29
|
+
bbstrader-0.1.92.dist-info/METADATA,sha256=LZdzZUHKait-lIvprJ85AxfRuHp8VE5QPS69y8uZIIs,9932
|
|
30
|
+
bbstrader-0.1.92.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
31
|
+
bbstrader-0.1.92.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
|
|
32
|
+
bbstrader-0.1.92.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|