bbstrader 0.2.97__py3-none-any.whl → 0.2.99__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/__main__.py +1 -1
- bbstrader/btengine/backtest.py +7 -7
- bbstrader/btengine/event.py +12 -4
- bbstrader/btengine/execution.py +3 -3
- bbstrader/btengine/portfolio.py +3 -3
- bbstrader/btengine/strategy.py +12 -5
- bbstrader/metatrader/account.py +30 -6
- bbstrader/metatrader/analysis.py +98 -0
- bbstrader/metatrader/copier.py +77 -57
- bbstrader/metatrader/trade.py +82 -118
- bbstrader/metatrader/utils.py +16 -0
- bbstrader/models/factors.py +97 -97
- bbstrader/trading/execution.py +686 -566
- bbstrader/trading/scripts.py +10 -11
- bbstrader/trading/strategies.py +13 -5
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/METADATA +12 -16
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/RECORD +21 -20
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.2.97.dist-info → bbstrader-0.2.99.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -137,6 +137,21 @@ class TradeSignal:
|
|
|
137
137
|
)
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
141
|
+
Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
142
|
+
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
143
|
+
Orders = Literal[
|
|
144
|
+
"all",
|
|
145
|
+
"buy_stops",
|
|
146
|
+
"sell_stops",
|
|
147
|
+
"buy_limits",
|
|
148
|
+
"sell_limits",
|
|
149
|
+
"buy_stop_limits",
|
|
150
|
+
"sell_stop_limits",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
EXPERT_ID = 98181105
|
|
154
|
+
|
|
140
155
|
class Trade(RiskManagement):
|
|
141
156
|
"""
|
|
142
157
|
Extends the `RiskManagement` class to include specific trading operations,
|
|
@@ -195,7 +210,7 @@ class Trade(RiskManagement):
|
|
|
195
210
|
self,
|
|
196
211
|
symbol: str = "EURUSD",
|
|
197
212
|
expert_name: str = "bbstrader",
|
|
198
|
-
expert_id: int =
|
|
213
|
+
expert_id: int = EXPERT_ID,
|
|
199
214
|
version: str = "2.0",
|
|
200
215
|
target: float = 5.0,
|
|
201
216
|
start_time: str = "0:00",
|
|
@@ -239,15 +254,12 @@ class Trade(RiskManagement):
|
|
|
239
254
|
See the ``bbstrader.metatrader.risk.RiskManagement`` class for more details on these parameters.
|
|
240
255
|
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
241
256
|
"""
|
|
242
|
-
# Call the parent class constructor first
|
|
243
257
|
super().__init__(
|
|
244
258
|
symbol=symbol,
|
|
245
259
|
start_time=start_time,
|
|
246
260
|
finishing_time=finishing_time,
|
|
247
|
-
**kwargs,
|
|
261
|
+
**kwargs,
|
|
248
262
|
)
|
|
249
|
-
|
|
250
|
-
# Initialize Trade-specific attributes
|
|
251
263
|
self.symbol = symbol
|
|
252
264
|
self.expert_name = expert_name
|
|
253
265
|
self.expert_id = expert_id
|
|
@@ -261,12 +273,6 @@ class Trade(RiskManagement):
|
|
|
261
273
|
self.tf = kwargs.get("time_frame", "D1")
|
|
262
274
|
self.kwargs = kwargs
|
|
263
275
|
|
|
264
|
-
self.start_time_hour, self.start_time_minutes = self.start.split(":")
|
|
265
|
-
self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
|
|
266
|
-
":"
|
|
267
|
-
)
|
|
268
|
-
self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
|
|
269
|
-
|
|
270
276
|
self.buy_positions = []
|
|
271
277
|
self.sell_positions = []
|
|
272
278
|
self.opened_positions = []
|
|
@@ -371,21 +377,18 @@ class Trade(RiskManagement):
|
|
|
371
377
|
|
|
372
378
|
def summary(self):
|
|
373
379
|
"""Show a brief description about the trading program"""
|
|
380
|
+
start = datetime.strptime(self.start, "%H:%M").time()
|
|
381
|
+
finish = datetime.strptime(self.finishing, "%H:%M").time()
|
|
382
|
+
end = datetime.strptime(self.end, "%H:%M").time()
|
|
374
383
|
summary_data = [
|
|
375
384
|
["Expert Advisor Name", f"@{self.expert_name}"],
|
|
376
385
|
["Expert Advisor Version", f"@{self.version}"],
|
|
377
386
|
["Expert | Strategy ID", self.expert_id],
|
|
378
387
|
["Trading Symbol", self.symbol],
|
|
379
388
|
["Trading Time Frame", self.tf],
|
|
380
|
-
["Start Trading Time", f"{
|
|
381
|
-
[
|
|
382
|
-
|
|
383
|
-
f"{self.finishing_time_hour}:{self.finishing_time_minutes}",
|
|
384
|
-
],
|
|
385
|
-
[
|
|
386
|
-
"Closing Position After",
|
|
387
|
-
f"{self.ending_time_hour}:{self.ending_time_minutes}",
|
|
388
|
-
],
|
|
389
|
+
["Start Trading Time", f"{start}"],
|
|
390
|
+
["Finishing Trading Time", f"{finish}"],
|
|
391
|
+
["Closing Position After", f"{end}"],
|
|
389
392
|
]
|
|
390
393
|
# Custom table format
|
|
391
394
|
summary_table = tabulate(
|
|
@@ -480,18 +483,14 @@ class Trade(RiskManagement):
|
|
|
480
483
|
session_data, headers=["Statistics", "Values"], tablefmt="outline"
|
|
481
484
|
)
|
|
482
485
|
|
|
483
|
-
# Print the formatted statistics
|
|
484
486
|
if self.verbose:
|
|
485
487
|
print("\n[======= Trading Session Statistics =======]")
|
|
486
488
|
print(session_table)
|
|
487
489
|
|
|
488
|
-
|
|
489
|
-
if save:
|
|
490
|
+
if save and stats["deals"] > 0:
|
|
490
491
|
today_date = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
491
|
-
# Create a dictionary with the statistics
|
|
492
492
|
statistics_dict = {item[0]: item[1] for item in session_data}
|
|
493
493
|
stats_df = pd.DataFrame(statistics_dict, index=[0])
|
|
494
|
-
# Create the directory if it doesn't exist
|
|
495
494
|
dir = dir or ".sessions"
|
|
496
495
|
os.makedirs(dir, exist_ok=True)
|
|
497
496
|
if "." in self.symbol:
|
|
@@ -504,8 +503,6 @@ class Trade(RiskManagement):
|
|
|
504
503
|
stats_df.to_csv(filepath, index=False)
|
|
505
504
|
LOGGER.info(f"Session statistics saved to {filepath}")
|
|
506
505
|
|
|
507
|
-
Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
508
|
-
|
|
509
506
|
def open_buy_position(
|
|
510
507
|
self,
|
|
511
508
|
action: Buys = "BMKT",
|
|
@@ -521,7 +518,7 @@ class Trade(RiskManagement):
|
|
|
521
518
|
tp: Optional[float] = None,
|
|
522
519
|
):
|
|
523
520
|
"""
|
|
524
|
-
Open a Buy
|
|
521
|
+
Open a Buy position
|
|
525
522
|
|
|
526
523
|
Args:
|
|
527
524
|
action (str): `BMKT` for Market orders or `BLMT`,
|
|
@@ -534,6 +531,8 @@ class Trade(RiskManagement):
|
|
|
534
531
|
mm (bool): Weither to put stop loss and tp or not
|
|
535
532
|
trail (bool): Weither to trail the stop loss or not
|
|
536
533
|
comment (str): The comment for the opening position
|
|
534
|
+
sl (float): The stop loss price
|
|
535
|
+
tp (float): The take profit price
|
|
537
536
|
"""
|
|
538
537
|
Id = id if id is not None else self.expert_id
|
|
539
538
|
point = self.get_symbol_info(self.symbol).point
|
|
@@ -583,7 +582,7 @@ class Trade(RiskManagement):
|
|
|
583
582
|
return False
|
|
584
583
|
|
|
585
584
|
def _order_type(self):
|
|
586
|
-
|
|
585
|
+
return {
|
|
587
586
|
"BMKT": (Mt5.ORDER_TYPE_BUY, "BUY"),
|
|
588
587
|
"SMKT": (Mt5.ORDER_TYPE_BUY, "SELL"),
|
|
589
588
|
"BLMT": (Mt5.ORDER_TYPE_BUY_LIMIT, "BUY_LIMIT"),
|
|
@@ -593,9 +592,6 @@ class Trade(RiskManagement):
|
|
|
593
592
|
"BSTPLMT": (Mt5.ORDER_TYPE_BUY_STOP_LIMIT, "BUY_STOP_LIMIT"),
|
|
594
593
|
"SSTPLMT": (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, "SELL_STOP_LIMIT"),
|
|
595
594
|
}
|
|
596
|
-
return type
|
|
597
|
-
|
|
598
|
-
Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
599
595
|
|
|
600
596
|
def open_sell_position(
|
|
601
597
|
self,
|
|
@@ -612,7 +608,7 @@ class Trade(RiskManagement):
|
|
|
612
608
|
tp: Optional[float] = None,
|
|
613
609
|
):
|
|
614
610
|
"""
|
|
615
|
-
Open a sell
|
|
611
|
+
Open a sell position
|
|
616
612
|
|
|
617
613
|
Args:
|
|
618
614
|
action (str): `SMKT` for Market orders
|
|
@@ -627,8 +623,8 @@ class Trade(RiskManagement):
|
|
|
627
623
|
comment (str): The comment for the closing position
|
|
628
624
|
symbol (str): The symbol to trade
|
|
629
625
|
volume (float): The volume (lot) to trade
|
|
630
|
-
sl (float): The stop loss
|
|
631
|
-
tp (float): The take profit
|
|
626
|
+
sl (float): The stop loss price
|
|
627
|
+
tp (float): The take profit price
|
|
632
628
|
"""
|
|
633
629
|
Id = id if id is not None else self.expert_id
|
|
634
630
|
point = self.get_symbol_info(self.symbol).point
|
|
@@ -694,11 +690,11 @@ class Trade(RiskManagement):
|
|
|
694
690
|
LOGGER.info(f"Not Trading time, SYMBOL={self.symbol}")
|
|
695
691
|
return False
|
|
696
692
|
elif not self.is_risk_ok():
|
|
697
|
-
LOGGER.
|
|
693
|
+
LOGGER.warning(f"Account Risk not allowed, SYMBOL={self.symbol}")
|
|
698
694
|
self._check(comment)
|
|
699
695
|
return False
|
|
700
696
|
elif self.is_max_trades_reached():
|
|
701
|
-
LOGGER.
|
|
697
|
+
LOGGER.warning(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
|
|
702
698
|
return False
|
|
703
699
|
elif self.profit_target():
|
|
704
700
|
self._check(f"Profit target Reached !!! SYMBOL={self.symbol}")
|
|
@@ -838,7 +834,8 @@ class Trade(RiskManagement):
|
|
|
838
834
|
action (str): (`'BMKT'`, `'SMKT'`) for Market orders
|
|
839
835
|
or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
|
|
840
836
|
price (float): The price at which to open an order
|
|
841
|
-
stoplimit (float): A price a pending Limit order is set at
|
|
837
|
+
stoplimit (float): A price a pending Limit order is set at
|
|
838
|
+
when the price reaches the 'price' value (this condition is mandatory).
|
|
842
839
|
The pending order is not passed to the trading system until that moment
|
|
843
840
|
id (int): The strategy id or expert Id
|
|
844
841
|
mm (bool): Weither to put stop loss and tp or not
|
|
@@ -846,43 +843,32 @@ class Trade(RiskManagement):
|
|
|
846
843
|
comment (str): The comment for the closing position
|
|
847
844
|
symbol (str): The symbol to trade
|
|
848
845
|
volume (float): The volume (lot) to trade
|
|
849
|
-
sl (float): The stop loss
|
|
850
|
-
tp (float): The take profit
|
|
846
|
+
sl (float): The stop loss price
|
|
847
|
+
tp (float): The take profit price
|
|
851
848
|
"""
|
|
852
849
|
BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
853
850
|
SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
854
851
|
if action in BUYS:
|
|
855
|
-
|
|
856
|
-
action=action,
|
|
857
|
-
price=price,
|
|
858
|
-
stoplimit=stoplimit,
|
|
859
|
-
id=id,
|
|
860
|
-
mm=mm,
|
|
861
|
-
trail=trail,
|
|
862
|
-
comment=comment,
|
|
863
|
-
symbol=symbol,
|
|
864
|
-
volume=volume,
|
|
865
|
-
sl=sl,
|
|
866
|
-
tp=tp,
|
|
867
|
-
)
|
|
852
|
+
open_position = self.open_buy_position
|
|
868
853
|
elif action in SELLS:
|
|
869
|
-
|
|
870
|
-
action=action,
|
|
871
|
-
price=price,
|
|
872
|
-
stoplimit=stoplimit,
|
|
873
|
-
id=id,
|
|
874
|
-
mm=mm,
|
|
875
|
-
trail=trail,
|
|
876
|
-
comment=comment,
|
|
877
|
-
symbol=symbol,
|
|
878
|
-
volume=volume,
|
|
879
|
-
sl=sl,
|
|
880
|
-
tp=tp,
|
|
881
|
-
)
|
|
854
|
+
open_position = self.open_sell_position
|
|
882
855
|
else:
|
|
883
856
|
raise ValueError(
|
|
884
857
|
f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}"
|
|
885
858
|
)
|
|
859
|
+
return open_position(
|
|
860
|
+
action=action,
|
|
861
|
+
price=price,
|
|
862
|
+
stoplimit=stoplimit,
|
|
863
|
+
id=id,
|
|
864
|
+
mm=mm,
|
|
865
|
+
trail=trail,
|
|
866
|
+
comment=comment,
|
|
867
|
+
symbol=symbol,
|
|
868
|
+
volume=volume,
|
|
869
|
+
sl=sl,
|
|
870
|
+
tp=tp,
|
|
871
|
+
)
|
|
886
872
|
|
|
887
873
|
@property
|
|
888
874
|
def orders(self):
|
|
@@ -1137,9 +1123,9 @@ class Trade(RiskManagement):
|
|
|
1137
1123
|
be = self.get_break_even()
|
|
1138
1124
|
if trail_after_points is not None:
|
|
1139
1125
|
if isinstance(trail_after_points, int):
|
|
1140
|
-
assert
|
|
1141
|
-
|
|
1142
|
-
)
|
|
1126
|
+
assert (
|
|
1127
|
+
trail_after_points > be
|
|
1128
|
+
), "trail_after_points must be greater than break even or set to None"
|
|
1143
1129
|
trail_after_points = self._get_trail_after_points(trail_after_points)
|
|
1144
1130
|
if positions is not None:
|
|
1145
1131
|
for position in positions:
|
|
@@ -1228,7 +1214,8 @@ class Trade(RiskManagement):
|
|
|
1228
1214
|
Sets the break-even level for a given trading position.
|
|
1229
1215
|
|
|
1230
1216
|
Args:
|
|
1231
|
-
position (TradePosition): The trading position for which the break-even is to be set.
|
|
1217
|
+
position (TradePosition): The trading position for which the break-even is to be set.
|
|
1218
|
+
This is the value return by `mt5.positions_get()`.
|
|
1232
1219
|
be (int): The break-even level in points.
|
|
1233
1220
|
level (float): The break-even level in price, if set to None , it will be calated automaticaly.
|
|
1234
1221
|
price (float): The break-even price, if set to None , it will be calated automaticaly.
|
|
@@ -1370,13 +1357,16 @@ class Trade(RiskManagement):
|
|
|
1370
1357
|
profit = 0.0
|
|
1371
1358
|
balance = self.get_account_info().balance
|
|
1372
1359
|
target = round((balance * self.target) / 100, 2)
|
|
1373
|
-
|
|
1374
|
-
|
|
1360
|
+
opened_positions = self.get_today_deals(group=self.symbol)
|
|
1361
|
+
if len(opened_positions) != 0:
|
|
1362
|
+
for position in opened_positions:
|
|
1375
1363
|
time.sleep(0.1)
|
|
1376
1364
|
# This return two TradeDeal Object,
|
|
1377
1365
|
# The first one is the opening order
|
|
1378
1366
|
# The second is the closing order
|
|
1379
|
-
history = self.get_trades_history(
|
|
1367
|
+
history = self.get_trades_history(
|
|
1368
|
+
position=position.position_id, to_df=False
|
|
1369
|
+
)
|
|
1380
1370
|
if history is not None and len(history) == 2:
|
|
1381
1371
|
profit += history[1].profit
|
|
1382
1372
|
commission += history[0].commission
|
|
@@ -1456,7 +1446,8 @@ class Trade(RiskManagement):
|
|
|
1456
1446
|
Args:
|
|
1457
1447
|
ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
|
|
1458
1448
|
price (float): The price at which to modify the order
|
|
1459
|
-
stoplimit (float): A price a pending Limit order is set at
|
|
1449
|
+
stoplimit (float): A price a pending Limit order is set at
|
|
1450
|
+
when the price reaches the 'price' value (this condition is mandatory).
|
|
1460
1451
|
The pending order is not passed to the trading system until that moment
|
|
1461
1452
|
sl (float): The stop loss in points
|
|
1462
1453
|
tp (float): The take profit in points
|
|
@@ -1598,16 +1589,6 @@ class Trade(RiskManagement):
|
|
|
1598
1589
|
f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}."
|
|
1599
1590
|
)
|
|
1600
1591
|
|
|
1601
|
-
Orders = Literal[
|
|
1602
|
-
"all",
|
|
1603
|
-
"buy_stops",
|
|
1604
|
-
"sell_stops",
|
|
1605
|
-
"buy_limits",
|
|
1606
|
-
"sell_limits",
|
|
1607
|
-
"buy_stop_limits",
|
|
1608
|
-
"sell_stop_limits",
|
|
1609
|
-
]
|
|
1610
|
-
|
|
1611
1592
|
def close_orders(
|
|
1612
1593
|
self,
|
|
1613
1594
|
order_type: Orders,
|
|
@@ -1616,7 +1597,8 @@ class Trade(RiskManagement):
|
|
|
1616
1597
|
):
|
|
1617
1598
|
"""
|
|
1618
1599
|
Args:
|
|
1619
|
-
order_type (str): Type of orders to close
|
|
1600
|
+
order_type (str): Type of orders to close
|
|
1601
|
+
('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
|
|
1620
1602
|
id (int): The unique ID of the Expert or Strategy
|
|
1621
1603
|
comment (str): Comment for the closing position
|
|
1622
1604
|
"""
|
|
@@ -1642,8 +1624,6 @@ class Trade(RiskManagement):
|
|
|
1642
1624
|
orders, "orders", self.close_order, order_type, id=id, comment=comment
|
|
1643
1625
|
)
|
|
1644
1626
|
|
|
1645
|
-
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
1646
|
-
|
|
1647
1627
|
def close_positions(
|
|
1648
1628
|
self,
|
|
1649
1629
|
position_type: Positions,
|
|
@@ -1689,13 +1669,11 @@ class Trade(RiskManagement):
|
|
|
1689
1669
|
List[TradeDeal]: List of today deals
|
|
1690
1670
|
"""
|
|
1691
1671
|
date_from = datetime.now() - timedelta(days=2)
|
|
1692
|
-
history =
|
|
1672
|
+
history = (
|
|
1673
|
+
self.get_trades_history(date_from=date_from, group=group, to_df=False) or []
|
|
1674
|
+
)
|
|
1693
1675
|
positions_ids = set(
|
|
1694
|
-
[
|
|
1695
|
-
deal.position_id
|
|
1696
|
-
for deal in history
|
|
1697
|
-
if history is not None and deal.magic == self.expert_id
|
|
1698
|
-
]
|
|
1676
|
+
[deal.position_id for deal in history if deal.magic == self.expert_id]
|
|
1699
1677
|
)
|
|
1700
1678
|
today_deals = []
|
|
1701
1679
|
for position in positions_ids:
|
|
@@ -1715,11 +1693,12 @@ class Trade(RiskManagement):
|
|
|
1715
1693
|
:return: bool
|
|
1716
1694
|
"""
|
|
1717
1695
|
negative_deals = 0
|
|
1696
|
+
max_trades = self.max_trade()
|
|
1718
1697
|
today_deals = self.get_today_deals(group=self.symbol)
|
|
1719
1698
|
for deal in today_deals:
|
|
1720
1699
|
if deal.profit < 0:
|
|
1721
1700
|
negative_deals += 1
|
|
1722
|
-
if negative_deals >=
|
|
1701
|
+
if negative_deals >= max_trades:
|
|
1723
1702
|
return True
|
|
1724
1703
|
return False
|
|
1725
1704
|
|
|
@@ -1800,7 +1779,6 @@ class Trade(RiskManagement):
|
|
|
1800
1779
|
The function assumes that the returns are the excess of
|
|
1801
1780
|
those compared to a benchmark.
|
|
1802
1781
|
"""
|
|
1803
|
-
# Get total history
|
|
1804
1782
|
import warnings
|
|
1805
1783
|
|
|
1806
1784
|
warnings.filterwarnings("ignore")
|
|
@@ -1817,33 +1795,19 @@ class Trade(RiskManagement):
|
|
|
1817
1795
|
|
|
1818
1796
|
def days_end(self) -> bool:
|
|
1819
1797
|
"""Check if it is the end of the trading day."""
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
ending_hour = int(self.ending_time_hour)
|
|
1824
|
-
ending_minute = int(self.ending_time_minutes)
|
|
1825
|
-
|
|
1826
|
-
if current_hour > ending_hour or (
|
|
1827
|
-
current_hour == ending_hour and current_minute >= ending_minute
|
|
1828
|
-
):
|
|
1798
|
+
now = datetime.now()
|
|
1799
|
+
end = datetime.strptime(self.end, "%H:%M").time()
|
|
1800
|
+
if now.time() >= end:
|
|
1829
1801
|
return True
|
|
1830
|
-
|
|
1831
|
-
return False
|
|
1802
|
+
return False
|
|
1832
1803
|
|
|
1833
1804
|
def trading_time(self):
|
|
1834
1805
|
"""Check if it is time to trade."""
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
):
|
|
1806
|
+
now = datetime.now()
|
|
1807
|
+
start = datetime.strptime(self.start, "%H:%M").time()
|
|
1808
|
+
end = datetime.strptime(self.finishing, "%H:%M").time()
|
|
1809
|
+
if start <= now.time() <= end:
|
|
1840
1810
|
return True
|
|
1841
|
-
elif datetime.now().hour == int(self.start_time_hour):
|
|
1842
|
-
if datetime.now().minute >= int(self.start_time_minutes):
|
|
1843
|
-
return True
|
|
1844
|
-
elif datetime.now().hour == int(self.finishing_time_hour):
|
|
1845
|
-
if datetime.now().minute < int(self.finishing_time_minutes):
|
|
1846
|
-
return True
|
|
1847
1811
|
return False
|
|
1848
1812
|
|
|
1849
1813
|
def sleep_time(self, weekend=False):
|
|
@@ -1943,7 +1907,7 @@ def create_trade_instance(
|
|
|
1943
1907
|
if ids is not None and isinstance(ids, (int, float))
|
|
1944
1908
|
else params["expert_id"]
|
|
1945
1909
|
if "expert_id" in params
|
|
1946
|
-
else
|
|
1910
|
+
else EXPERT_ID
|
|
1947
1911
|
)
|
|
1948
1912
|
params["pchange_sl"] = (
|
|
1949
1913
|
pchange_sl[symbol]
|
bbstrader/metatrader/utils.py
CHANGED
|
@@ -286,6 +286,22 @@ class TickInfo(NamedTuple):
|
|
|
286
286
|
volume_real: float
|
|
287
287
|
|
|
288
288
|
|
|
289
|
+
class BookInfo(NamedTuple):
|
|
290
|
+
"""
|
|
291
|
+
Represents the structure of a book.
|
|
292
|
+
* type: Type of the order (buy/sell)
|
|
293
|
+
* price: Price of the order
|
|
294
|
+
* volume: Volume of the order in lots
|
|
295
|
+
* volume_dbl: Volume with greater accuracy
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
type: int
|
|
300
|
+
price: float
|
|
301
|
+
volume: float
|
|
302
|
+
volume_dbl: float
|
|
303
|
+
|
|
304
|
+
|
|
289
305
|
class TradeRequest(NamedTuple):
|
|
290
306
|
"""
|
|
291
307
|
Represents a Trade Request Structure
|
bbstrader/models/factors.py
CHANGED
|
@@ -16,6 +16,101 @@ __all__ = [
|
|
|
16
16
|
"search_coint_candidate_pairs",
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
+
def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
|
|
20
|
+
"""Download and process data for a list of tickers from the specified source."""
|
|
21
|
+
data_list = []
|
|
22
|
+
for ticker in tickers:
|
|
23
|
+
try:
|
|
24
|
+
if source == "yf":
|
|
25
|
+
data = yf.download(
|
|
26
|
+
ticker,
|
|
27
|
+
start=start,
|
|
28
|
+
end=end,
|
|
29
|
+
progress=False,
|
|
30
|
+
multi_level_index=False,
|
|
31
|
+
)
|
|
32
|
+
data = data.drop(columns=["Adj Close"], axis=1)
|
|
33
|
+
elif source == "mt5":
|
|
34
|
+
start, end = pd.Timestamp(start), pd.Timestamp(end)
|
|
35
|
+
data = download_historical_data(
|
|
36
|
+
symbol=ticker,
|
|
37
|
+
timeframe=tf,
|
|
38
|
+
date_from=start,
|
|
39
|
+
date_to=end,
|
|
40
|
+
**{"path": path},
|
|
41
|
+
)
|
|
42
|
+
data = data.drop(columns=["adj_close"], axis=1)
|
|
43
|
+
elif source in ["fmp", "eodhd"]:
|
|
44
|
+
handler_class = (
|
|
45
|
+
FMPDataHandler if source == "fmp" else EODHDataHandler
|
|
46
|
+
)
|
|
47
|
+
handler = handler_class(events=None, symbol_list=[ticker], **kwargs)
|
|
48
|
+
data = handler.data[ticker]
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(f"Invalid source: {source}")
|
|
51
|
+
|
|
52
|
+
data = data.reset_index()
|
|
53
|
+
data = data.rename(columns=str.lower)
|
|
54
|
+
data["ticker"] = ticker
|
|
55
|
+
data_list.append(data)
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"No Data found for {ticker}: {e}")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
return pd.concat(data_list)
|
|
62
|
+
|
|
63
|
+
def _handle_date_range(start, end, window):
|
|
64
|
+
"""Handle start and end date generation."""
|
|
65
|
+
if start is None or end is None:
|
|
66
|
+
end = pd.Timestamp(datetime.now()).strftime("%Y-%m-%d")
|
|
67
|
+
start = (
|
|
68
|
+
pd.Timestamp(datetime.now())
|
|
69
|
+
- pd.DateOffset(years=window)
|
|
70
|
+
+ pd.DateOffset(days=1)
|
|
71
|
+
).strftime("%Y-%m-%d")
|
|
72
|
+
return start, end
|
|
73
|
+
|
|
74
|
+
def _period_search(start, end, securities, candidates, window, npairs):
|
|
75
|
+
if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"The date range must be at least two (2) years for period search."
|
|
78
|
+
)
|
|
79
|
+
top_pairs = []
|
|
80
|
+
p_start = pd.Timestamp(end) - pd.DateOffset(years=1)
|
|
81
|
+
periods = pd.date_range(start=p_start, end=pd.Timestamp(end), freq="BQE")
|
|
82
|
+
npairs = max(round(npairs / 2), 1)
|
|
83
|
+
for period in periods:
|
|
84
|
+
s_start = period - pd.DateOffset(years=2) + pd.DateOffset(days=1)
|
|
85
|
+
print(f"Searching for pairs in period: {s_start} - {period}")
|
|
86
|
+
pairs = find_cointegrated_pairs(
|
|
87
|
+
securities,
|
|
88
|
+
candidates,
|
|
89
|
+
n=npairs,
|
|
90
|
+
start=str(s_start),
|
|
91
|
+
stop=str(period),
|
|
92
|
+
coint=True,
|
|
93
|
+
)
|
|
94
|
+
pairs["period"] = period
|
|
95
|
+
top_pairs.append(pairs)
|
|
96
|
+
top_pairs = pd.concat(top_pairs)
|
|
97
|
+
if len(top_pairs.columns) <= 1:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"No pairs found in the specified period."
|
|
100
|
+
"Please adjust the date range or increase the number of pairs."
|
|
101
|
+
)
|
|
102
|
+
return top_pairs.head(npairs * 2)
|
|
103
|
+
|
|
104
|
+
def _process_asset_data(securities, candidates, universe, rolling_window):
|
|
105
|
+
"""Process and select assets from the data."""
|
|
106
|
+
securities = select_assets(
|
|
107
|
+
securities, n=universe, rolling_window=rolling_window
|
|
108
|
+
)
|
|
109
|
+
candidates = select_assets(
|
|
110
|
+
candidates, n=universe, rolling_window=rolling_window
|
|
111
|
+
)
|
|
112
|
+
return securities, candidates
|
|
113
|
+
|
|
19
114
|
|
|
20
115
|
def search_coint_candidate_pairs(
|
|
21
116
|
securities: pd.DataFrame | List[str] = None,
|
|
@@ -145,101 +240,6 @@ def search_coint_candidate_pairs(
|
|
|
145
240
|
|
|
146
241
|
"""
|
|
147
242
|
|
|
148
|
-
def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
|
|
149
|
-
"""Download and process data for a list of tickers from the specified source."""
|
|
150
|
-
data_list = []
|
|
151
|
-
for ticker in tickers:
|
|
152
|
-
try:
|
|
153
|
-
if source == "yf":
|
|
154
|
-
data = yf.download(
|
|
155
|
-
ticker,
|
|
156
|
-
start=start,
|
|
157
|
-
end=end,
|
|
158
|
-
progress=False,
|
|
159
|
-
multi_level_index=False,
|
|
160
|
-
)
|
|
161
|
-
data = data.drop(columns=["Adj Close"], axis=1)
|
|
162
|
-
elif source == "mt5":
|
|
163
|
-
start, end = pd.Timestamp(start), pd.Timestamp(end)
|
|
164
|
-
data = download_historical_data(
|
|
165
|
-
symbol=ticker,
|
|
166
|
-
timeframe=tf,
|
|
167
|
-
date_from=start,
|
|
168
|
-
date_to=end,
|
|
169
|
-
**{"path": path},
|
|
170
|
-
)
|
|
171
|
-
data = data.drop(columns=["adj_close"], axis=1)
|
|
172
|
-
elif source in ["fmp", "eodhd"]:
|
|
173
|
-
handler_class = (
|
|
174
|
-
FMPDataHandler if source == "fmp" else EODHDataHandler
|
|
175
|
-
)
|
|
176
|
-
handler = handler_class(events=None, symbol_list=[ticker], **kwargs)
|
|
177
|
-
data = handler.data[ticker]
|
|
178
|
-
else:
|
|
179
|
-
raise ValueError(f"Invalid source: {source}")
|
|
180
|
-
|
|
181
|
-
data = data.reset_index()
|
|
182
|
-
data = data.rename(columns=str.lower)
|
|
183
|
-
data["ticker"] = ticker
|
|
184
|
-
data_list.append(data)
|
|
185
|
-
|
|
186
|
-
except Exception as e:
|
|
187
|
-
print(f"No Data found for {ticker}: {e}")
|
|
188
|
-
continue
|
|
189
|
-
|
|
190
|
-
return pd.concat(data_list)
|
|
191
|
-
|
|
192
|
-
def _handle_date_range(start, end, window):
|
|
193
|
-
"""Handle start and end date generation."""
|
|
194
|
-
if start is None or end is None:
|
|
195
|
-
end = pd.Timestamp(datetime.now()).strftime("%Y-%m-%d")
|
|
196
|
-
start = (
|
|
197
|
-
pd.Timestamp(datetime.now())
|
|
198
|
-
- pd.DateOffset(years=window)
|
|
199
|
-
+ pd.DateOffset(days=1)
|
|
200
|
-
).strftime("%Y-%m-%d")
|
|
201
|
-
return start, end
|
|
202
|
-
|
|
203
|
-
def _period_search(start, end, securities, candidates, npairs=npairs):
|
|
204
|
-
if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
|
|
205
|
-
raise ValueError(
|
|
206
|
-
"The date range must be at least two (2) years for period search."
|
|
207
|
-
)
|
|
208
|
-
top_pairs = []
|
|
209
|
-
p_start = pd.Timestamp(end) - pd.DateOffset(years=1)
|
|
210
|
-
periods = pd.date_range(start=p_start, end=pd.Timestamp(end), freq="BQE")
|
|
211
|
-
npairs = max(round(npairs / 2), 1)
|
|
212
|
-
for period in periods:
|
|
213
|
-
s_start = period - pd.DateOffset(years=2) + pd.DateOffset(days=1)
|
|
214
|
-
print(f"Searching for pairs in period: {s_start} - {period}")
|
|
215
|
-
pairs = find_cointegrated_pairs(
|
|
216
|
-
securities,
|
|
217
|
-
candidates,
|
|
218
|
-
n=npairs,
|
|
219
|
-
start=str(s_start),
|
|
220
|
-
stop=str(period),
|
|
221
|
-
coint=True,
|
|
222
|
-
)
|
|
223
|
-
pairs["period"] = period
|
|
224
|
-
top_pairs.append(pairs)
|
|
225
|
-
top_pairs = pd.concat(top_pairs)
|
|
226
|
-
if len(top_pairs.columns) <= 1:
|
|
227
|
-
raise ValueError(
|
|
228
|
-
"No pairs found in the specified period."
|
|
229
|
-
"Please adjust the date range or increase the number of pairs."
|
|
230
|
-
)
|
|
231
|
-
return top_pairs.head(npairs * 2)
|
|
232
|
-
|
|
233
|
-
def _process_asset_data(securities, candidates, universe, rolling_window):
|
|
234
|
-
"""Process and select assets from the data."""
|
|
235
|
-
securities = select_assets(
|
|
236
|
-
securities, n=universe, rolling_window=rolling_window
|
|
237
|
-
)
|
|
238
|
-
candidates = select_assets(
|
|
239
|
-
candidates, n=universe, rolling_window=rolling_window
|
|
240
|
-
)
|
|
241
|
-
return securities, candidates
|
|
242
|
-
|
|
243
243
|
if (
|
|
244
244
|
securities is not None
|
|
245
245
|
and candidates is not None
|
|
@@ -255,7 +255,7 @@ def search_coint_candidate_pairs(
|
|
|
255
255
|
if period_search:
|
|
256
256
|
start = securities.index.get_level_values("date").min()
|
|
257
257
|
end = securities.index.get_level_values("date").max()
|
|
258
|
-
top_pairs = _period_search(start, end, securities, candidates)
|
|
258
|
+
top_pairs = _period_search(start, end, securities, candidates, window, npairs)
|
|
259
259
|
else:
|
|
260
260
|
top_pairs = find_cointegrated_pairs(
|
|
261
261
|
securities, candidates, n=npairs, coint=True
|
|
@@ -291,7 +291,7 @@ def search_coint_candidate_pairs(
|
|
|
291
291
|
)
|
|
292
292
|
if period_search:
|
|
293
293
|
top_pairs = _period_search(
|
|
294
|
-
start, end, securities_data, candidates_data
|
|
294
|
+
start, end, securities_data, candidates_data, window, npairs
|
|
295
295
|
).head(npairs)
|
|
296
296
|
else:
|
|
297
297
|
top_pairs = find_cointegrated_pairs(
|