bbstrader 0.1.6__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 +97 -702
- bbstrader/btengine/data.py +3 -1
- bbstrader/btengine/event.py +15 -9
- bbstrader/btengine/execution.py +66 -6
- bbstrader/btengine/performance.py +34 -1
- bbstrader/btengine/portfolio.py +24 -14
- bbstrader/btengine/strategy.py +1 -2
- bbstrader/metatrader/account.py +4 -4
- bbstrader/metatrader/rates.py +6 -1
- bbstrader/metatrader/trade.py +45 -32
- bbstrader/models/risk.py +39 -2
- bbstrader/trading/__init__.py +8 -1
- bbstrader/trading/execution.py +337 -928
- bbstrader/trading/strategies.py +838 -0
- bbstrader/tseries.py +603 -19
- {bbstrader-0.1.6.dist-info → bbstrader-0.1.7.dist-info}/METADATA +15 -7
- bbstrader-0.1.7.dist-info/RECORD +26 -0
- {bbstrader-0.1.6.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.6.dist-info/RECORD +0 -28
- {bbstrader-0.1.6.dist-info → bbstrader-0.1.7.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.6.dist-info → bbstrader-0.1.7.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import csv
|
|
3
3
|
import time
|
|
4
|
-
import logging
|
|
5
4
|
import numpy as np
|
|
6
5
|
from datetime import datetime
|
|
7
6
|
import MetaTrader5 as Mt5
|
|
7
|
+
from logging import Logger
|
|
8
8
|
from typing import List, Tuple, Dict, Any, Optional, Literal
|
|
9
9
|
from bbstrader.metatrader.risk import RiskManagement
|
|
10
10
|
from bbstrader.metatrader.account import INIT_MSG
|
|
@@ -13,9 +13,6 @@ from bbstrader.metatrader.utils import (
|
|
|
13
13
|
raise_mt5_error, trade_retcode_message, config_logger
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
# Configure the logger
|
|
17
|
-
logger = config_logger('trade.log', console_log=False)
|
|
18
|
-
|
|
19
16
|
class Trade(RiskManagement):
|
|
20
17
|
"""
|
|
21
18
|
Extends the `RiskManagement` class to include specific trading operations,
|
|
@@ -81,6 +78,8 @@ class Trade(RiskManagement):
|
|
|
81
78
|
finishing_time: str = "23:00",
|
|
82
79
|
ending_time: str = "23:30",
|
|
83
80
|
verbose: Optional[bool] = None,
|
|
81
|
+
console_log: Optional[bool] = False,
|
|
82
|
+
logger: Logger | str = 'bbstrader.log',
|
|
84
83
|
**kwargs,
|
|
85
84
|
):
|
|
86
85
|
"""
|
|
@@ -98,6 +97,8 @@ class Trade(RiskManagement):
|
|
|
98
97
|
ending_time (str): The time after which any open position will be closed.
|
|
99
98
|
verbose (bool | None): If set to None (default), account summary and risk managment
|
|
100
99
|
parameters are printed in the terminal.
|
|
100
|
+
console_log (bool): If set to True, log messages are displayed in the console.
|
|
101
|
+
logger (Logger | str): The logger object to use for logging messages could be a string or a logger object.
|
|
101
102
|
|
|
102
103
|
Inherits:
|
|
103
104
|
- max_risk
|
|
@@ -131,6 +132,8 @@ class Trade(RiskManagement):
|
|
|
131
132
|
self.start = start_time
|
|
132
133
|
self.end = ending_time
|
|
133
134
|
self.finishing = finishing_time
|
|
135
|
+
self.console_log = console_log
|
|
136
|
+
self.logger = self._get_logger(logger, console_log)
|
|
134
137
|
self.tf = kwargs.get("time_frame", 'D1')
|
|
135
138
|
|
|
136
139
|
self.lot = self.get_lot()
|
|
@@ -162,6 +165,12 @@ class Trade(RiskManagement):
|
|
|
162
165
|
print(
|
|
163
166
|
f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
|
|
164
167
|
|
|
168
|
+
def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
|
|
169
|
+
"""Get the logger object"""
|
|
170
|
+
if isinstance(logger, str):
|
|
171
|
+
return config_logger(logger, consol_log)
|
|
172
|
+
return logger
|
|
173
|
+
|
|
165
174
|
def initialize(self):
|
|
166
175
|
"""
|
|
167
176
|
Initializes the MetaTrader 5 (MT5) terminal for trading operations.
|
|
@@ -183,7 +192,7 @@ class Trade(RiskManagement):
|
|
|
183
192
|
f" Version @{self.version}, on {self.symbol}."
|
|
184
193
|
)
|
|
185
194
|
except Exception as e:
|
|
186
|
-
logger.error(f"During initialization: {e}")
|
|
195
|
+
self.logger.error(f"During initialization: {e}")
|
|
187
196
|
|
|
188
197
|
def select_symbol(self):
|
|
189
198
|
"""
|
|
@@ -200,7 +209,7 @@ class Trade(RiskManagement):
|
|
|
200
209
|
if not Mt5.symbol_select(self.symbol, True):
|
|
201
210
|
raise_mt5_error(message=INIT_MSG)
|
|
202
211
|
except Exception as e:
|
|
203
|
-
logger.error(f"Selecting symbol '{self.symbol}': {e}")
|
|
212
|
+
self.logger.error(f"Selecting symbol '{self.symbol}': {e}")
|
|
204
213
|
|
|
205
214
|
def prepare_symbol(self):
|
|
206
215
|
"""
|
|
@@ -222,7 +231,7 @@ class Trade(RiskManagement):
|
|
|
222
231
|
if self.verbose:
|
|
223
232
|
print("Initialization successfully completed.")
|
|
224
233
|
except Exception as e:
|
|
225
|
-
logger.error(f"Preparing symbol '{self.symbol}': {e}")
|
|
234
|
+
self.logger.error(f"Preparing symbol '{self.symbol}': {e}")
|
|
226
235
|
|
|
227
236
|
def summary(self):
|
|
228
237
|
"""Show a brief description about the trading program"""
|
|
@@ -327,7 +336,7 @@ class Trade(RiskManagement):
|
|
|
327
336
|
|
|
328
337
|
# Save to CSV if specified
|
|
329
338
|
if save:
|
|
330
|
-
today_date = datetime.now().strftime(
|
|
339
|
+
today_date = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
331
340
|
# Create a dictionary with the statistics
|
|
332
341
|
statistics_dict = {
|
|
333
342
|
"Total Trades": deals,
|
|
@@ -351,7 +360,7 @@ class Trade(RiskManagement):
|
|
|
351
360
|
else:
|
|
352
361
|
symbol = self.symbol
|
|
353
362
|
|
|
354
|
-
filename = f"{symbol}_{today_date}_session.csv"
|
|
363
|
+
filename = f"{symbol}_{today_date}@{self.expert_id}_session.csv"
|
|
355
364
|
filepath = os.path.join(dir, filename)
|
|
356
365
|
|
|
357
366
|
# Updated code to write to CSV
|
|
@@ -362,7 +371,7 @@ class Trade(RiskManagement):
|
|
|
362
371
|
writer.writerow(["Statistic", "Value"])
|
|
363
372
|
for stat, value in statistics_dict.items():
|
|
364
373
|
writer.writerow([stat, value])
|
|
365
|
-
logger.info(f"Session statistics saved to {filepath}")
|
|
374
|
+
self.logger.info(f"Session statistics saved to {filepath}")
|
|
366
375
|
|
|
367
376
|
def open_buy_position(
|
|
368
377
|
self,
|
|
@@ -508,14 +517,14 @@ class Trade(RiskManagement):
|
|
|
508
517
|
if self.days_end():
|
|
509
518
|
return False
|
|
510
519
|
elif not self.trading_time():
|
|
511
|
-
logger.info(f"Not Trading time, SYMBOL={self.symbol}")
|
|
520
|
+
self.logger.info(f"Not Trading time, SYMBOL={self.symbol}")
|
|
512
521
|
return False
|
|
513
522
|
elif not self.is_risk_ok():
|
|
514
|
-
logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
|
|
523
|
+
self.logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
|
|
515
524
|
self._check(comment)
|
|
516
525
|
return False
|
|
517
526
|
elif not self._risk_free():
|
|
518
|
-
logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
|
|
527
|
+
self.logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
|
|
519
528
|
self._check(comment)
|
|
520
529
|
return False
|
|
521
530
|
elif self.profit_target():
|
|
@@ -525,7 +534,7 @@ class Trade(RiskManagement):
|
|
|
525
534
|
def _check(self, txt: str = ""):
|
|
526
535
|
if self.positive_profit() or self.get_current_open_positions() is None:
|
|
527
536
|
self.close_positions(position_type='all')
|
|
528
|
-
logger.info(txt)
|
|
537
|
+
self.logger.info(txt)
|
|
529
538
|
time.sleep(5)
|
|
530
539
|
self.statistics(save=True)
|
|
531
540
|
|
|
@@ -560,7 +569,7 @@ class Trade(RiskManagement):
|
|
|
560
569
|
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
561
570
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
562
571
|
msg = trade_retcode_message(result.retcode)
|
|
563
|
-
logger.error(
|
|
572
|
+
self.logger.error(
|
|
564
573
|
f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
565
574
|
if result.retcode in [
|
|
566
575
|
Mt5.TRADE_RETCODE_CONNECTION, Mt5.TRADE_RETCODE_TIMEOUT]:
|
|
@@ -580,7 +589,7 @@ class Trade(RiskManagement):
|
|
|
580
589
|
# Print the result
|
|
581
590
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
582
591
|
msg = trade_retcode_message(result.retcode)
|
|
583
|
-
logger.info(f"Trade Order {msg}{addtionnal}")
|
|
592
|
+
self.logger.info(f"Trade Order {msg}{addtionnal}")
|
|
584
593
|
if type != "BMKT" or type != "SMKT":
|
|
585
594
|
self.opened_orders.append(result.order)
|
|
586
595
|
long_msg = (
|
|
@@ -588,7 +597,7 @@ class Trade(RiskManagement):
|
|
|
588
597
|
f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
|
|
589
598
|
f"Tp: {self.get_take_profit()}"
|
|
590
599
|
)
|
|
591
|
-
logger.info(long_msg)
|
|
600
|
+
self.logger.info(long_msg)
|
|
592
601
|
time.sleep(0.1)
|
|
593
602
|
if type == "BMKT" or type == "SMKT":
|
|
594
603
|
self.opened_positions.append(result.order)
|
|
@@ -606,12 +615,12 @@ class Trade(RiskManagement):
|
|
|
606
615
|
f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open,5)}, "
|
|
607
616
|
f"Sl: @{position.sl} Tp: @{position.tp}"
|
|
608
617
|
)
|
|
609
|
-
logger.info(order_info)
|
|
618
|
+
self.logger.info(order_info)
|
|
610
619
|
pos_info = (
|
|
611
620
|
f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
|
|
612
621
|
f"{self.get_account_info().currency}]\n"
|
|
613
622
|
)
|
|
614
|
-
logger.info(pos_info)
|
|
623
|
+
self.logger.info(pos_info)
|
|
615
624
|
|
|
616
625
|
def open_position(
|
|
617
626
|
self,
|
|
@@ -908,7 +917,7 @@ class Trade(RiskManagement):
|
|
|
908
917
|
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
909
918
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
910
919
|
msg = trade_retcode_message(result.retcode)
|
|
911
|
-
logger.error(
|
|
920
|
+
self.logger.error(
|
|
912
921
|
f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
913
922
|
tries = 0
|
|
914
923
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
|
|
@@ -928,11 +937,11 @@ class Trade(RiskManagement):
|
|
|
928
937
|
tries += 1
|
|
929
938
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
930
939
|
msg = trade_retcode_message(result.retcode)
|
|
931
|
-
logger.info(f"Break-Even Order {msg}{addtionnal}")
|
|
940
|
+
self.logger.info(f"Break-Even Order {msg}{addtionnal}")
|
|
932
941
|
info = (
|
|
933
942
|
f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
|
|
934
943
|
)
|
|
935
|
-
logger.info(info)
|
|
944
|
+
self.logger.info(info)
|
|
936
945
|
self.break_even_status.append(tiket)
|
|
937
946
|
|
|
938
947
|
def win_trade(self,
|
|
@@ -1049,7 +1058,7 @@ class Trade(RiskManagement):
|
|
|
1049
1058
|
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1050
1059
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1051
1060
|
msg = trade_retcode_message(result.retcode)
|
|
1052
|
-
logger.error(
|
|
1061
|
+
self.logger.error(
|
|
1053
1062
|
f"Closing Order Request, Position: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
1054
1063
|
tries = 0
|
|
1055
1064
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
@@ -1066,11 +1075,11 @@ class Trade(RiskManagement):
|
|
|
1066
1075
|
tries += 1
|
|
1067
1076
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1068
1077
|
msg = trade_retcode_message(result.retcode)
|
|
1069
|
-
logger.info(
|
|
1078
|
+
self.logger.info(
|
|
1070
1079
|
f"Closing Order {msg}{addtionnal}")
|
|
1071
1080
|
info = (
|
|
1072
1081
|
f"Position #{ticket} closed, Symbol: {self.symbol}, Price: @{request['price']}")
|
|
1073
|
-
logger.info(info)
|
|
1082
|
+
self.logger.info(info)
|
|
1074
1083
|
return True
|
|
1075
1084
|
else:
|
|
1076
1085
|
return False
|
|
@@ -1098,13 +1107,16 @@ class Trade(RiskManagement):
|
|
|
1098
1107
|
|
|
1099
1108
|
if positions is not None:
|
|
1100
1109
|
if position_type == 'all':
|
|
1101
|
-
pos_type = ""
|
|
1102
1110
|
tickets = [position.ticket for position in positions]
|
|
1103
1111
|
else:
|
|
1104
1112
|
tickets = positions
|
|
1105
|
-
pos_type = position_type
|
|
1106
1113
|
else:
|
|
1107
1114
|
tickets = []
|
|
1115
|
+
|
|
1116
|
+
if position_type == 'all':
|
|
1117
|
+
pos_type = ''
|
|
1118
|
+
else:
|
|
1119
|
+
pos_type = position_type
|
|
1108
1120
|
|
|
1109
1121
|
if len(tickets) != 0:
|
|
1110
1122
|
for ticket in tickets.copy():
|
|
@@ -1113,13 +1125,13 @@ class Trade(RiskManagement):
|
|
|
1113
1125
|
time.sleep(1)
|
|
1114
1126
|
|
|
1115
1127
|
if len(tickets) == 0:
|
|
1116
|
-
logger.info(
|
|
1128
|
+
self.logger.info(
|
|
1117
1129
|
f"ALL {pos_type.upper()} Positions closed, SYMBOL={self.symbol}.")
|
|
1118
1130
|
else:
|
|
1119
|
-
logger.info(
|
|
1131
|
+
self.logger.info(
|
|
1120
1132
|
f"{len(tickets)} {pos_type.upper()} Positions not closed, SYMBOL={self.symbol}")
|
|
1121
1133
|
else:
|
|
1122
|
-
logger.info(
|
|
1134
|
+
self.logger.info(
|
|
1123
1135
|
f"No {pos_type.upper()} Positions to close, SYMBOL={self.symbol}.")
|
|
1124
1136
|
|
|
1125
1137
|
def get_stats(self) -> Tuple[Dict[str, Any]]:
|
|
@@ -1277,7 +1289,8 @@ class Trade(RiskManagement):
|
|
|
1277
1289
|
|
|
1278
1290
|
def create_trade_instance(
|
|
1279
1291
|
symbols: List[str],
|
|
1280
|
-
params: Dict[str, Any]
|
|
1292
|
+
params: Dict[str, Any],
|
|
1293
|
+
logger: Logger = ...) -> Dict[str, Trade]:
|
|
1281
1294
|
"""
|
|
1282
1295
|
Creates Trade instances for each symbol provided.
|
|
1283
1296
|
|
|
@@ -1292,7 +1305,6 @@ def create_trade_instance(
|
|
|
1292
1305
|
ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
|
|
1293
1306
|
"""
|
|
1294
1307
|
instances = {}
|
|
1295
|
-
|
|
1296
1308
|
if not symbols:
|
|
1297
1309
|
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1298
1310
|
for symbol in symbols:
|
|
@@ -1300,4 +1312,5 @@ def create_trade_instance(
|
|
|
1300
1312
|
instances[symbol] = Trade(**params, symbol=symbol)
|
|
1301
1313
|
except Exception as e:
|
|
1302
1314
|
logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1315
|
+
assert len(instances) == len(symbols), "Failed to create Trade instances for all symbols."
|
|
1303
1316
|
return instances
|
bbstrader/models/risk.py
CHANGED
|
@@ -7,12 +7,14 @@ from hmmlearn.hmm import GaussianHMM
|
|
|
7
7
|
from abc import ABCMeta, abstractmethod
|
|
8
8
|
from matplotlib import cm, pyplot as plt
|
|
9
9
|
from matplotlib.dates import YearLocator, MonthLocator
|
|
10
|
-
from typing import Optional
|
|
10
|
+
from typing import Optional, Dict
|
|
11
|
+
from bbstrader.metatrader.rates import Rates
|
|
11
12
|
sns.set_theme()
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"RiskModel",
|
|
15
|
-
"HMMRiskManager"
|
|
16
|
+
"HMMRiskManager",
|
|
17
|
+
"build_hmm_models"
|
|
16
18
|
]
|
|
17
19
|
|
|
18
20
|
|
|
@@ -347,3 +349,38 @@ class HMMRiskManager(RiskModel):
|
|
|
347
349
|
ax.xaxis.set_minor_locator(MonthLocator())
|
|
348
350
|
ax.grid(True)
|
|
349
351
|
plt.show()
|
|
352
|
+
|
|
353
|
+
def build_hmm_models(symbol_list=None, **kwargs
|
|
354
|
+
) -> Dict[str, HMMRiskManager]:
|
|
355
|
+
mt5_data = kwargs.get("use_mt5_data", False)
|
|
356
|
+
data = kwargs.get("hmm_data")
|
|
357
|
+
tf = kwargs.get("time_frame", 'D1')
|
|
358
|
+
hmm_end = kwargs.get("hmm_end", 0)
|
|
359
|
+
sd = kwargs.get("session_duration", 23.0)
|
|
360
|
+
hmm_tickers = kwargs.get("hmm_tickers")
|
|
361
|
+
if hmm_tickers is not None:
|
|
362
|
+
symbols = hmm_tickers
|
|
363
|
+
else:
|
|
364
|
+
symbols = symbol_list
|
|
365
|
+
hmm_models = {symbol: None for symbol in symbols}
|
|
366
|
+
if data is not None:
|
|
367
|
+
if isinstance(data, pd.DataFrame):
|
|
368
|
+
hmm_data = data
|
|
369
|
+
hmm = HMMRiskManager(
|
|
370
|
+
data=hmm_data, verbose=True, iterations=1000, **kwargs)
|
|
371
|
+
for symbol in symbols:
|
|
372
|
+
hmm_models[symbol] = hmm
|
|
373
|
+
elif isinstance(data, dict):
|
|
374
|
+
for symbol, data in data.items():
|
|
375
|
+
hmm = HMMRiskManager(
|
|
376
|
+
data=data, verbose=True, iterations=1000, **kwargs)
|
|
377
|
+
hmm_models[symbol] = hmm
|
|
378
|
+
if mt5_data:
|
|
379
|
+
for symbol in symbols:
|
|
380
|
+
rates = Rates(symbol, tf, start_pos=hmm_end, session_duration=sd)
|
|
381
|
+
data = rates.get_rates_from_pos()
|
|
382
|
+
assert data is not None, f"No data for {symbol}"
|
|
383
|
+
hmm = HMMRiskManager(
|
|
384
|
+
data=data, verbose=True, iterations=1000, **kwargs)
|
|
385
|
+
hmm_models[symbol] = hmm
|
|
386
|
+
return hmm_models
|
bbstrader/trading/__init__.py
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Trading strategies execution module
|
|
3
|
+
|
|
4
|
+
This module is responsible for executing trading strategies.
|
|
5
|
+
It provides a framework for executing trading strategies and managing the trading process.
|
|
6
|
+
The module is designed to be flexible and extensible, allowing users to define their own trading
|
|
7
|
+
strategies and customize the trading process.
|
|
8
|
+
|
|
3
9
|
"""
|
|
4
|
-
from bbstrader.trading.
|
|
10
|
+
from bbstrader.trading.execution import ExecutionEngine
|
|
11
|
+
from bbstrader.trading.strategies import *
|