bbstrader 0.3.3__py3-none-any.whl → 0.3.5__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/__init__.py +1 -1
- bbstrader/__main__.py +10 -2
- bbstrader/apps/__init__.py +0 -0
- bbstrader/apps/_copier.py +664 -0
- bbstrader/btengine/strategy.py +163 -90
- bbstrader/compat.py +18 -10
- bbstrader/config.py +0 -16
- bbstrader/core/scripts.py +4 -3
- bbstrader/core/utils.py +5 -3
- bbstrader/metatrader/account.py +169 -29
- bbstrader/metatrader/analysis.py +7 -5
- bbstrader/metatrader/copier.py +61 -14
- bbstrader/metatrader/scripts.py +15 -2
- bbstrader/metatrader/trade.py +28 -24
- bbstrader/metatrader/utils.py +64 -0
- bbstrader/models/factors.py +17 -13
- bbstrader/models/ml.py +104 -54
- bbstrader/trading/execution.py +9 -8
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/METADATA +25 -28
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/RECORD +24 -22
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.3.dist-info → bbstrader-0.3.5.dist-info}/top_level.txt +0 -0
bbstrader/btengine/strategy.py
CHANGED
|
@@ -11,10 +11,12 @@ from loguru import logger
|
|
|
11
11
|
|
|
12
12
|
from bbstrader.btengine.data import DataHandler
|
|
13
13
|
from bbstrader.btengine.event import Events, FillEvent, SignalEvent
|
|
14
|
+
from bbstrader.metatrader.trade import generate_signal, TradeAction
|
|
14
15
|
from bbstrader.config import BBSTRADER_DIR
|
|
15
16
|
from bbstrader.metatrader import (
|
|
16
17
|
Account,
|
|
17
18
|
AdmiralMarktsGroup,
|
|
19
|
+
MetaQuotes,
|
|
18
20
|
PepperstoneGroupLimited,
|
|
19
21
|
TradeOrder,
|
|
20
22
|
Rates,
|
|
@@ -78,7 +80,16 @@ class MT5Strategy(Strategy):
|
|
|
78
80
|
in order to avoid naming collusion.
|
|
79
81
|
"""
|
|
80
82
|
tf: str
|
|
83
|
+
id: int
|
|
84
|
+
ID: int
|
|
85
|
+
|
|
81
86
|
max_trades: Dict[str, int]
|
|
87
|
+
risk_budget: Dict[str, float] | str | None
|
|
88
|
+
|
|
89
|
+
_orders: Dict[str, Dict[str, List[SignalEvent]]]
|
|
90
|
+
_positions: Dict[str, Dict[str, int | float]]
|
|
91
|
+
_trades: Dict[str, Dict[str, int]]
|
|
92
|
+
|
|
82
93
|
def __init__(
|
|
83
94
|
self,
|
|
84
95
|
events: Queue = None,
|
|
@@ -104,13 +115,19 @@ class MT5Strategy(Strategy):
|
|
|
104
115
|
self.data = bars
|
|
105
116
|
self.symbols = symbol_list
|
|
106
117
|
self.mode = mode
|
|
107
|
-
self.
|
|
118
|
+
if self.mode not in [TradingMode.BACKTEST, TradingMode.LIVE]:
|
|
119
|
+
raise ValueError(f"Mode must be an instance of {type(TradingMode)} not {type(self.mode)}")
|
|
120
|
+
|
|
108
121
|
self.risk_budget = self._check_risk_budget(**kwargs)
|
|
122
|
+
|
|
109
123
|
self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
|
|
110
124
|
self.tf = kwargs.get("time_frame", "D1")
|
|
111
125
|
self.logger = kwargs.get("logger") or logger
|
|
126
|
+
|
|
112
127
|
if self.mode == TradingMode.BACKTEST:
|
|
128
|
+
self._porfolio_value = None
|
|
113
129
|
self._initialize_portfolio()
|
|
130
|
+
|
|
114
131
|
self.kwargs = kwargs
|
|
115
132
|
self.periodes = 0
|
|
116
133
|
|
|
@@ -170,19 +187,17 @@ class MT5Strategy(Strategy):
|
|
|
170
187
|
return weights
|
|
171
188
|
|
|
172
189
|
def _initialize_portfolio(self):
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
self.
|
|
176
|
-
self._positions: Dict[str, Dict[str, int | float]] = {}
|
|
177
|
-
self._trades: Dict[str, Dict[str, int]] = {}
|
|
190
|
+
self._orders = {}
|
|
191
|
+
self._positions = {}
|
|
192
|
+
self._trades = {}
|
|
178
193
|
for symbol in self.symbols:
|
|
179
194
|
self._positions[symbol] = {}
|
|
180
195
|
self._orders[symbol] = {}
|
|
181
196
|
self._trades[symbol] = {}
|
|
182
|
-
for position in
|
|
197
|
+
for position in ["LONG", "SHORT"]:
|
|
183
198
|
self._trades[symbol][position] = 0
|
|
184
199
|
self._positions[symbol][position] = 0.0
|
|
185
|
-
for order in
|
|
200
|
+
for order in ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]:
|
|
186
201
|
self._orders[symbol][order] = []
|
|
187
202
|
self._holdings = {s: 0.0 for s in self.symbols}
|
|
188
203
|
|
|
@@ -236,6 +251,54 @@ class MT5Strategy(Strategy):
|
|
|
236
251
|
"""
|
|
237
252
|
pass
|
|
238
253
|
|
|
254
|
+
def signal(self, signal: int, symbol: str) -> TradeSignal:
|
|
255
|
+
"""
|
|
256
|
+
Generate a ``TradeSignal`` object based on the signal value.
|
|
257
|
+
Args:
|
|
258
|
+
signal : An integer value representing the signal type:
|
|
259
|
+
0: BUY
|
|
260
|
+
1: SELL
|
|
261
|
+
2: EXIT_LONG
|
|
262
|
+
3: EXIT_SHORT
|
|
263
|
+
4: EXIT_ALL_POSITIONS
|
|
264
|
+
5: EXIT_ALL_ORDERS
|
|
265
|
+
6: EXIT_STOP
|
|
266
|
+
7: EXIT_LIMIT
|
|
267
|
+
|
|
268
|
+
symbol : The symbol for the trade.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
TradeSignal : A ``TradeSignal`` object representing the trade signal.
|
|
272
|
+
|
|
273
|
+
Note:
|
|
274
|
+
This generate only common signals. For more complex signals, use `generate_signal` directly.
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
ValueError : If the signal value is not between 0 and 7.
|
|
278
|
+
"""
|
|
279
|
+
signal_id = getattr(self, "id", None) or getattr(self, "ID")
|
|
280
|
+
|
|
281
|
+
match signal:
|
|
282
|
+
case 0:
|
|
283
|
+
return generate_signal(signal_id, symbol, TradeAction.BUY)
|
|
284
|
+
case 1:
|
|
285
|
+
return generate_signal(signal_id, symbol, TradeAction.SELL)
|
|
286
|
+
case 2:
|
|
287
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_LONG)
|
|
288
|
+
case 3:
|
|
289
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_SHORT)
|
|
290
|
+
case 4:
|
|
291
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_ALL_POSITIONS)
|
|
292
|
+
case 5:
|
|
293
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_ALL_ORDERS)
|
|
294
|
+
case 6:
|
|
295
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_STOP)
|
|
296
|
+
case 7:
|
|
297
|
+
return generate_signal(signal_id, symbol, TradeAction.EXIT_LIMIT)
|
|
298
|
+
case _:
|
|
299
|
+
raise ValueError(f"Invalid signal value: {signal}. Must be an integer between 0 and 7.")
|
|
300
|
+
|
|
301
|
+
|
|
239
302
|
def perform_period_end_checks(self, *args, **kwargs):
|
|
240
303
|
"""
|
|
241
304
|
Some strategies may require additional checks at the end of the period,
|
|
@@ -551,75 +614,75 @@ class MT5Strategy(Strategy):
|
|
|
551
614
|
]
|
|
552
615
|
logmsg(order, log_label, symbol, dtime)
|
|
553
616
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
617
|
+
for symbol in self.symbols:
|
|
618
|
+
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
619
|
+
latest_close = self.data.get_latest_bar_value(symbol, "close")
|
|
620
|
+
|
|
621
|
+
process_orders(
|
|
622
|
+
"BLMT",
|
|
623
|
+
lambda o: latest_close <= o.price,
|
|
624
|
+
lambda o: self.buy_mkt(
|
|
625
|
+
o.strategy_id, symbol, o.price, o.quantity, dtime
|
|
626
|
+
),
|
|
627
|
+
"BUY LIMIT",
|
|
628
|
+
symbol,
|
|
629
|
+
dtime,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
process_orders(
|
|
633
|
+
"SLMT",
|
|
634
|
+
lambda o: latest_close >= o.price,
|
|
635
|
+
lambda o: self.sell_mkt(
|
|
636
|
+
o.strategy_id, symbol, o.price, o.quantity, dtime
|
|
637
|
+
),
|
|
638
|
+
"SELL LIMIT",
|
|
639
|
+
symbol,
|
|
640
|
+
dtime,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
process_orders(
|
|
644
|
+
"BSTP",
|
|
645
|
+
lambda o: latest_close >= o.price,
|
|
646
|
+
lambda o: self.buy_mkt(
|
|
647
|
+
o.strategy_id, symbol, o.price, o.quantity, dtime
|
|
648
|
+
),
|
|
649
|
+
"BUY STOP",
|
|
650
|
+
symbol,
|
|
651
|
+
dtime,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
process_orders(
|
|
655
|
+
"SSTP",
|
|
656
|
+
lambda o: latest_close <= o.price,
|
|
657
|
+
lambda o: self.sell_mkt(
|
|
658
|
+
o.strategy_id, symbol, o.price, o.quantity, dtime
|
|
659
|
+
),
|
|
660
|
+
"SELL STOP",
|
|
661
|
+
symbol,
|
|
662
|
+
dtime,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
process_orders(
|
|
666
|
+
"BSTPLMT",
|
|
667
|
+
lambda o: latest_close >= o.price,
|
|
668
|
+
lambda o: self.buy_limit(
|
|
669
|
+
o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
|
|
670
|
+
),
|
|
671
|
+
"BUY STOP LIMIT",
|
|
672
|
+
symbol,
|
|
673
|
+
dtime,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
process_orders(
|
|
677
|
+
"SSTPLMT",
|
|
678
|
+
lambda o: latest_close <= o.price,
|
|
679
|
+
lambda o: self.sell_limit(
|
|
680
|
+
o.strategy_id, symbol, o.stoplimit, o.quantity, dtime
|
|
681
|
+
),
|
|
682
|
+
"SELL STOP LIMIT",
|
|
683
|
+
symbol,
|
|
684
|
+
dtime,
|
|
685
|
+
)
|
|
623
686
|
|
|
624
687
|
@staticmethod
|
|
625
688
|
def calculate_pct_change(current_price, lh_price) -> float:
|
|
@@ -803,13 +866,10 @@ class MT5Strategy(Strategy):
|
|
|
803
866
|
elif len(prices) in range(2, self.max_trades[asset] + 1):
|
|
804
867
|
price = np.mean(prices)
|
|
805
868
|
if price is not None:
|
|
806
|
-
if
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
and abs(self.calculate_pct_change(bid, price)) >= th
|
|
811
|
-
):
|
|
812
|
-
return True
|
|
869
|
+
if position == 0:
|
|
870
|
+
return self.calculate_pct_change(ask, price) >= th
|
|
871
|
+
elif position == 1:
|
|
872
|
+
return self.calculate_pct_change(bid, price) <= -th
|
|
813
873
|
return False
|
|
814
874
|
|
|
815
875
|
@staticmethod
|
|
@@ -834,9 +894,7 @@ class MT5Strategy(Strategy):
|
|
|
834
894
|
dt_to : The converted datetime.
|
|
835
895
|
"""
|
|
836
896
|
from_tz = pytz.timezone(from_tz)
|
|
837
|
-
if isinstance(dt, datetime):
|
|
838
|
-
dt = pd.to_datetime(dt, unit="s")
|
|
839
|
-
elif isinstance(dt, int):
|
|
897
|
+
if isinstance(dt, (datetime, int)):
|
|
840
898
|
dt = pd.to_datetime(dt, unit="s")
|
|
841
899
|
if dt.tzinfo is None:
|
|
842
900
|
dt = dt.tz_localize(from_tz)
|
|
@@ -862,20 +920,35 @@ class MT5Strategy(Strategy):
|
|
|
862
920
|
Returns:
|
|
863
921
|
mt5_equivalent : The MetaTrader 5 equivalent symbols for the symbols in the list.
|
|
864
922
|
"""
|
|
923
|
+
|
|
865
924
|
account = Account(**kwargs)
|
|
866
925
|
mt5_symbols = account.get_symbols(symbol_type=symbol_type)
|
|
867
926
|
mt5_equivalent = []
|
|
868
|
-
|
|
927
|
+
|
|
928
|
+
def _get_admiral_symbols():
|
|
869
929
|
for s in mt5_symbols:
|
|
870
930
|
_s = s[1:] if s[0] in string.punctuation else s
|
|
871
931
|
for symbol in symbols:
|
|
872
932
|
if _s.split(".")[0] == symbol or _s.split("_")[0] == symbol:
|
|
873
933
|
mt5_equivalent.append(s)
|
|
874
|
-
|
|
875
|
-
|
|
934
|
+
|
|
935
|
+
def _get_pepperstone_symbols():
|
|
936
|
+
for s in mt5_symbols:
|
|
876
937
|
for symbol in symbols:
|
|
877
938
|
if s.split(".")[0] == symbol:
|
|
878
939
|
mt5_equivalent.append(s)
|
|
940
|
+
|
|
941
|
+
if account.broker == MetaQuotes():
|
|
942
|
+
if "Admiral" in account.server:
|
|
943
|
+
_get_admiral_symbols()
|
|
944
|
+
elif "Pepperstone" in account.server:
|
|
945
|
+
_get_pepperstone_symbols()
|
|
946
|
+
|
|
947
|
+
elif account.broker == AdmiralMarktsGroup():
|
|
948
|
+
_get_admiral_symbols()
|
|
949
|
+
elif account.broker == PepperstoneGroupLimited():
|
|
950
|
+
_get_pepperstone_symbols()
|
|
951
|
+
|
|
879
952
|
return mt5_equivalent
|
|
880
953
|
|
|
881
954
|
|
bbstrader/compat.py
CHANGED
|
@@ -2,18 +2,26 @@ import platform
|
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def
|
|
6
|
-
"""Mock
|
|
5
|
+
def setup_mock_modules():
|
|
6
|
+
"""Mock some modules not available on some OS to prevent import errors."""
|
|
7
|
+
from unittest.mock import MagicMock
|
|
8
|
+
|
|
9
|
+
class Mock(MagicMock):
|
|
10
|
+
@classmethod
|
|
11
|
+
def __getattr__(cls, name):
|
|
12
|
+
return MagicMock()
|
|
13
|
+
|
|
14
|
+
MOCK_MODULES = []
|
|
15
|
+
|
|
16
|
+
# Mock Metatrader5 on Linux and MacOS
|
|
7
17
|
if platform.system() != "Windows":
|
|
8
|
-
|
|
18
|
+
MOCK_MODULES.append("MetaTrader5")
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return MagicMock()
|
|
20
|
+
# Mock posix On windows
|
|
21
|
+
if platform.system() == "Windows":
|
|
22
|
+
MOCK_MODULES.append("posix")
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
|
24
|
+
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
|
17
25
|
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
setup_mock_modules()
|
bbstrader/config.py
CHANGED
|
@@ -3,22 +3,6 @@ from pathlib import Path
|
|
|
3
3
|
from typing import List
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
TERMINAL = "/terminal64.exe"
|
|
7
|
-
BASE_FOLDER = "C:/Program Files/"
|
|
8
|
-
|
|
9
|
-
AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
|
|
10
|
-
PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
|
|
11
|
-
FTMO_PATH = BASE_FOLDER + "FTMO MetaTrader 5" + TERMINAL
|
|
12
|
-
JGM_PATH = BASE_FOLDER + "JustMarkets MetaTrader 5" + TERMINAL
|
|
13
|
-
|
|
14
|
-
BROKERS_PATHS = {
|
|
15
|
-
"AMG": AMG_PATH,
|
|
16
|
-
"FTMO": FTMO_PATH,
|
|
17
|
-
"PGL": PGL_PATH,
|
|
18
|
-
"JGM": JGM_PATH,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
6
|
def get_config_dir(name: str = ".bbstrader") -> Path:
|
|
23
7
|
"""
|
|
24
8
|
Get the path to the configuration directory.
|
bbstrader/core/scripts.py
CHANGED
|
@@ -141,18 +141,19 @@ def send_news_feed(unknown):
|
|
|
141
141
|
|
|
142
142
|
nltk.download("punkt", quiet=True)
|
|
143
143
|
news = FinancialNews()
|
|
144
|
+
fmp_news = news.get_fmp_news(api=args.fmp) if args.fmp else None
|
|
144
145
|
logger.info(f"Starting the News Feed on {args.interval} minutes")
|
|
145
146
|
while True:
|
|
146
147
|
try:
|
|
147
148
|
fmp_articles = []
|
|
148
|
-
|
|
149
|
-
if args.fmp:
|
|
149
|
+
if fmp_news is not None:
|
|
150
150
|
start = datetime.now() - timedelta(minutes=args.interval)
|
|
151
151
|
start = start.strftime("%Y-%m-%d %H:%M:%S")
|
|
152
152
|
end = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
153
|
-
fmp_articles =
|
|
153
|
+
fmp_articles = fmp_news.get_latest_articles(
|
|
154
154
|
save=True, start=start, end=end
|
|
155
155
|
)
|
|
156
|
+
coindesk_articles = news.get_coindesk_news(query=args.query)
|
|
156
157
|
if len(coindesk_articles) != 0:
|
|
157
158
|
asyncio.run(
|
|
158
159
|
send_articles(
|
bbstrader/core/utils.py
CHANGED
|
@@ -66,9 +66,11 @@ def dict_from_ini(file_path, sections: str | List[str] = None) -> Dict[str, Any]
|
|
|
66
66
|
Returns:
|
|
67
67
|
A dictionary containing the INI file contents with proper data types.
|
|
68
68
|
"""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
try:
|
|
70
|
+
config = configparser.ConfigParser(interpolation=None)
|
|
71
|
+
config.read(file_path)
|
|
72
|
+
except Exception:
|
|
73
|
+
raise
|
|
72
74
|
ini_dict = {}
|
|
73
75
|
for section in config.sections():
|
|
74
76
|
ini_dict[section] = {
|