bbstrader 0.2.99__py3-none-any.whl → 0.3.0__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 → __init__.py} +0 -1
- bbstrader/__main__.py +31 -14
- bbstrader/btengine/data.py +4 -2
- bbstrader/btengine/event.py +15 -14
- bbstrader/btengine/execution.py +24 -14
- bbstrader/btengine/strategy.py +33 -15
- bbstrader/core/data.py +99 -3
- bbstrader/core/scripts.py +130 -0
- bbstrader/metatrader/account.py +124 -129
- bbstrader/metatrader/copier.py +5 -6
- bbstrader/metatrader/rates.py +14 -13
- bbstrader/metatrader/risk.py +13 -11
- bbstrader/metatrader/trade.py +34 -23
- bbstrader/metatrader/utils.py +79 -26
- bbstrader/models/factors.py +3 -1
- bbstrader/models/ml.py +2 -1
- bbstrader/models/nlp.py +21 -4
- bbstrader/trading/execution.py +36 -19
- bbstrader/trading/strategies.py +15 -14
- bbstrader/tseries.py +8 -9
- bbstrader-0.3.0.dist-info/METADATA +469 -0
- bbstrader-0.3.0.dist-info/RECORD +47 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/WHEEL +1 -1
- bbstrader-0.2.99.dist-info/METADATA +0 -193
- bbstrader-0.2.99.dist-info/RECORD +0 -46
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.2.99.dist-info → bbstrader-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import sys
|
|
4
|
+
import textwrap
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
import nltk
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from sumy.nlp.tokenizers import Tokenizer
|
|
11
|
+
from sumy.parsers.plaintext import PlaintextParser
|
|
12
|
+
from sumy.summarizers.text_rank import TextRankSummarizer
|
|
13
|
+
|
|
14
|
+
from bbstrader.core.data import FinancialNews
|
|
15
|
+
from bbstrader.trading.utils import send_telegram_message
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def summarize_text(text, sentences_count=5):
|
|
19
|
+
"""
|
|
20
|
+
Generate a summary using TextRank algorithm.
|
|
21
|
+
"""
|
|
22
|
+
parser = PlaintextParser.from_string(text, Tokenizer("english"))
|
|
23
|
+
summarizer = TextRankSummarizer()
|
|
24
|
+
summary = summarizer(parser.document, sentences_count)
|
|
25
|
+
return " ".join(str(sentence) for sentence in summary)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def format_article_for_telegram(article: dict) -> str:
|
|
29
|
+
if not all(
|
|
30
|
+
k in article
|
|
31
|
+
for k in (
|
|
32
|
+
"body",
|
|
33
|
+
"title",
|
|
34
|
+
"published_on",
|
|
35
|
+
"sentiment",
|
|
36
|
+
"keywords",
|
|
37
|
+
"keywords",
|
|
38
|
+
"url",
|
|
39
|
+
)
|
|
40
|
+
):
|
|
41
|
+
return ""
|
|
42
|
+
summary = summarize_text(article["body"])
|
|
43
|
+
text = (
|
|
44
|
+
f"📰 {article['title']}\n"
|
|
45
|
+
f"Published Date: {article['published_on']}\n"
|
|
46
|
+
f"Sentiment: {article['sentiment']}\n"
|
|
47
|
+
f"Status: {article['status']}\n"
|
|
48
|
+
f"Keywords: {article['keywords']}\n\n"
|
|
49
|
+
f"🔍 Summary\n"
|
|
50
|
+
f"{textwrap.fill(summary, width=80)}"
|
|
51
|
+
f"\n\n👉 Visit {article['url']} for full article."
|
|
52
|
+
)
|
|
53
|
+
return text
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def send_articles(articles: dict, token: str, id: str, interval=15):
|
|
57
|
+
for article in articles:
|
|
58
|
+
if article["published_on"] >= datetime.now() - timedelta(minutes=interval):
|
|
59
|
+
article["published_on"] = article.get("published_on").strftime(
|
|
60
|
+
"%Y-%m-%d %H:%M:%S"
|
|
61
|
+
)
|
|
62
|
+
message = format_article_for_telegram(article)
|
|
63
|
+
if message == "":
|
|
64
|
+
return
|
|
65
|
+
await send_telegram_message(token, id, text=message)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def send_news_feed(unknown):
|
|
69
|
+
HELP_MSG = """
|
|
70
|
+
Send news feed from Coindesk to Telegram channel.
|
|
71
|
+
This script fetches the latest news articles from Coindesk, summarizes them,
|
|
72
|
+
and sends them to a specified Telegram channel at regular intervals.
|
|
73
|
+
|
|
74
|
+
Usage:
|
|
75
|
+
python -m bbstrader --run news_feed [options]
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
-q, --query: The news to look for (default: "")
|
|
79
|
+
-t, --token: Telegram bot token
|
|
80
|
+
-I, --id: Telegram Chat id
|
|
81
|
+
-i, --interval: Interval in minutes to fetch news (default: 15)
|
|
82
|
+
|
|
83
|
+
Note:
|
|
84
|
+
The script will run indefinitely, fetching news every 15 minutes.
|
|
85
|
+
Use Ctrl+C to stop the script.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
if "-h" in unknown or "--help" in unknown:
|
|
89
|
+
print(HELP_MSG)
|
|
90
|
+
sys.exit(0)
|
|
91
|
+
|
|
92
|
+
parser = argparse.ArgumentParser()
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"-q", "--query", type=str, default="", help="The news to look for"
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"-t",
|
|
98
|
+
"--token",
|
|
99
|
+
type=str,
|
|
100
|
+
required=True,
|
|
101
|
+
help="Telegram bot token",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument("-I", "--id", type=str, required=True, help="Telegram Chat id")
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"-i",
|
|
106
|
+
"--interval",
|
|
107
|
+
type=int,
|
|
108
|
+
default=15,
|
|
109
|
+
help="Interval in minutes to fetch news (default: 15)",
|
|
110
|
+
)
|
|
111
|
+
args = parser.parse_args(unknown)
|
|
112
|
+
|
|
113
|
+
nltk.download("punkt", quiet=True)
|
|
114
|
+
news = FinancialNews()
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Starting the News Feed on {args.interval} minutes"
|
|
117
|
+
)
|
|
118
|
+
while True:
|
|
119
|
+
try:
|
|
120
|
+
articles = news.get_coindesk_news(query=args.query)
|
|
121
|
+
if len(articles) == 0:
|
|
122
|
+
time.sleep(args.interval * 60)
|
|
123
|
+
continue
|
|
124
|
+
asyncio.run(send_articles(articles, args.token, args.id))
|
|
125
|
+
time.sleep(args.interval * 60)
|
|
126
|
+
except KeyboardInterrupt:
|
|
127
|
+
logger.info("Stopping the News Feed ...")
|
|
128
|
+
exit(0)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(e)
|
bbstrader/metatrader/account.py
CHANGED
|
@@ -14,6 +14,7 @@ from bbstrader.metatrader.utils import (
|
|
|
14
14
|
OrderCheckResult,
|
|
15
15
|
OrderSentResult,
|
|
16
16
|
SymbolInfo,
|
|
17
|
+
SymbolType,
|
|
17
18
|
TerminalInfo,
|
|
18
19
|
TickInfo,
|
|
19
20
|
TradeDeal,
|
|
@@ -84,14 +85,14 @@ ftmo_url = _FTMO_URL_
|
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
_SYMBOLS_TYPE_ = {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
SymbolType.ETFs: r"\b(ETFs?)\b",
|
|
89
|
+
SymbolType.BONDS: r"\b(Treasuries?)\b",
|
|
90
|
+
SymbolType.FOREX: r"\b(Forex|Exotics?)\b",
|
|
91
|
+
SymbolType.FUTURES: r"\b(Futures?|Forwards)\b",
|
|
92
|
+
SymbolType.STOCKS: r"\b(Stocks?|Equities?|Shares?)\b",
|
|
93
|
+
SymbolType.INDICES: r"\b(?:Indices?|Cash|Index)\b(?!.*\\(?:UKOIL|USOIL))",
|
|
94
|
+
SymbolType.COMMODITIES: r"\b(Commodity|Commodities?|Metals?|Agricultures?|Energies?|OIL|Oil|USOIL|UKOIL)\b",
|
|
95
|
+
SymbolType.CRYPTO: r"\b(Cryptos?|Cryptocurrencies|Cryptocurrency)\b",
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
_COUNTRY_MAP_ = {
|
|
@@ -222,7 +223,7 @@ class Broker(object):
|
|
|
222
223
|
|
|
223
224
|
class AdmiralMarktsGroup(Broker):
|
|
224
225
|
def __init__(self, **kwargs):
|
|
225
|
-
super().__init__("
|
|
226
|
+
super().__init__(__BROKERS__["AMG"], **kwargs)
|
|
226
227
|
|
|
227
228
|
@property
|
|
228
229
|
def timezone(self) -> str:
|
|
@@ -231,7 +232,7 @@ class AdmiralMarktsGroup(Broker):
|
|
|
231
232
|
|
|
232
233
|
class JustGlobalMarkets(Broker):
|
|
233
234
|
def __init__(self, **kwargs):
|
|
234
|
-
super().__init__("
|
|
235
|
+
super().__init__(__BROKERS__["JGM"], **kwargs)
|
|
235
236
|
|
|
236
237
|
@property
|
|
237
238
|
def timezone(self) -> str:
|
|
@@ -240,7 +241,7 @@ class JustGlobalMarkets(Broker):
|
|
|
240
241
|
|
|
241
242
|
class FTMO(Broker):
|
|
242
243
|
def __init__(self, **kwargs):
|
|
243
|
-
super().__init__("FTMO
|
|
244
|
+
super().__init__(__BROKERS__["FTMO"], **kwargs)
|
|
244
245
|
|
|
245
246
|
@property
|
|
246
247
|
def timezone(self) -> str:
|
|
@@ -249,7 +250,7 @@ class FTMO(Broker):
|
|
|
249
250
|
|
|
250
251
|
class PepperstoneGroupLimited(Broker):
|
|
251
252
|
def __init__(self, **kwargs):
|
|
252
|
-
super().__init__("
|
|
253
|
+
super().__init__(__BROKERS__["PGL"], **kwargs)
|
|
253
254
|
|
|
254
255
|
@property
|
|
255
256
|
def timezone(self) -> str:
|
|
@@ -316,6 +317,8 @@ class Account(object):
|
|
|
316
317
|
def _check_brokers(self, **kwargs):
|
|
317
318
|
if kwargs.get("copy", False):
|
|
318
319
|
return
|
|
320
|
+
if kwargs.get("backtest", False):
|
|
321
|
+
return
|
|
319
322
|
supported = BROKERS.copy()
|
|
320
323
|
if self.broker not in supported.values():
|
|
321
324
|
msg = (
|
|
@@ -574,7 +577,7 @@ class Account(object):
|
|
|
574
577
|
|
|
575
578
|
def get_symbols(
|
|
576
579
|
self,
|
|
577
|
-
symbol_type="ALL",
|
|
580
|
+
symbol_type: SymbolType | str = "ALL",
|
|
578
581
|
check_etf=False,
|
|
579
582
|
save=False,
|
|
580
583
|
file_name="symbols",
|
|
@@ -585,16 +588,9 @@ class Account(object):
|
|
|
585
588
|
Get all specified financial instruments from the MetaTrader 5 terminal.
|
|
586
589
|
|
|
587
590
|
Args:
|
|
588
|
-
symbol_type (str) The
|
|
591
|
+
symbol_type (SymbolType | str): The type of financial instruments to retrieve.
|
|
589
592
|
- `ALL`: For all available symbols
|
|
590
|
-
- `
|
|
591
|
-
- `ETF`: ETFs (e.g., 'QQQ')
|
|
592
|
-
- `IDX`: Indices (e.g., 'SP500')
|
|
593
|
-
- `FX`: Forex pairs (e.g., 'EURUSD')
|
|
594
|
-
- `COMD`: Commodities (e.g., 'CRUDOIL', 'GOLD')
|
|
595
|
-
- `FUT`: Futures (e.g., 'USTNote_U4'),
|
|
596
|
-
- `CRYPTO`: Cryptocurrencies (e.g., 'BTC', 'ETH')
|
|
597
|
-
- `BOND`: Bonds (e.g., 'USTN10YR')
|
|
593
|
+
- See `bbstrader.metatrader.utils.SymbolType` for more details.
|
|
598
594
|
|
|
599
595
|
check_etf (bool): If True and symbol_type is 'etf', check if the
|
|
600
596
|
ETF description contains 'ETF'.
|
|
@@ -621,9 +617,21 @@ class Account(object):
|
|
|
621
617
|
patterns = _SYMBOLS_TYPE_
|
|
622
618
|
|
|
623
619
|
if symbol_type != "ALL":
|
|
624
|
-
if symbol_type not in patterns:
|
|
620
|
+
if not isinstance(symbol_type, SymbolType) or symbol_type not in patterns:
|
|
625
621
|
raise ValueError(f"Unsupported symbol type: {symbol_type}")
|
|
626
622
|
|
|
623
|
+
def check_etfs(info):
|
|
624
|
+
err_msg = (
|
|
625
|
+
f"{info.name} doesn't have 'ETF' in its description. "
|
|
626
|
+
"If this is intended, set check_etf=False."
|
|
627
|
+
)
|
|
628
|
+
if (
|
|
629
|
+
symbol_type == SymbolType.ETFs
|
|
630
|
+
and check_etf
|
|
631
|
+
and "ETF" not in info.description
|
|
632
|
+
):
|
|
633
|
+
raise ValueError(err_msg)
|
|
634
|
+
|
|
627
635
|
if save:
|
|
628
636
|
max_lengh = max([len(s.name) for s in symbols])
|
|
629
637
|
file_path = f"{file_name}.txt"
|
|
@@ -637,15 +645,7 @@ class Account(object):
|
|
|
637
645
|
pattern = re.compile(patterns[symbol_type])
|
|
638
646
|
match = re.search(pattern, info.path)
|
|
639
647
|
if match:
|
|
640
|
-
|
|
641
|
-
symbol_type == "ETF"
|
|
642
|
-
and check_etf
|
|
643
|
-
and "ETF" not in info.description
|
|
644
|
-
):
|
|
645
|
-
raise ValueError(
|
|
646
|
-
f"{info.name} doesn't have 'ETF' in its description. "
|
|
647
|
-
"If this is intended, set check_etf=False."
|
|
648
|
-
)
|
|
648
|
+
check_etfs(info)
|
|
649
649
|
self._write_symbol(file, info, include_desc, max_lengh)
|
|
650
650
|
symbol_list.append(s.name)
|
|
651
651
|
|
|
@@ -658,31 +658,16 @@ class Account(object):
|
|
|
658
658
|
pattern = re.compile(patterns[symbol_type]) # , re.IGNORECASE
|
|
659
659
|
match = re.search(pattern, info.path)
|
|
660
660
|
if match:
|
|
661
|
-
|
|
662
|
-
symbol_type == "ETF"
|
|
663
|
-
and check_etf
|
|
664
|
-
and "ETF" not in info.description
|
|
665
|
-
):
|
|
666
|
-
raise ValueError(
|
|
667
|
-
f"{info.name} doesn't have 'ETF' in its description. "
|
|
668
|
-
"If this is intended, set check_etf=False."
|
|
669
|
-
)
|
|
661
|
+
check_etfs(info)
|
|
670
662
|
symbol_list.append(s.name)
|
|
671
663
|
|
|
672
664
|
# Print a summary of the retrieved symbols
|
|
673
665
|
if display_total:
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
"
|
|
677
|
-
|
|
678
|
-
"
|
|
679
|
-
"FX": "Forex Paires",
|
|
680
|
-
"COMD": "Commodities",
|
|
681
|
-
"FUT": "Futures",
|
|
682
|
-
"CRYPTO": "Cryptos Assets",
|
|
683
|
-
"BOND": "Bonds",
|
|
684
|
-
}
|
|
685
|
-
print(f"Total {names[symbol_type]}: {len(symbol_list)}")
|
|
666
|
+
name = symbol_type if isinstance(symbol_type, str) else symbol_type.name
|
|
667
|
+
if symbol_type == "ALL":
|
|
668
|
+
print(f"Total number of symbols: {len(symbol_list)}")
|
|
669
|
+
else:
|
|
670
|
+
print(f"Total number of {name} symbols: {len(symbol_list)}")
|
|
686
671
|
|
|
687
672
|
return symbol_list
|
|
688
673
|
|
|
@@ -694,9 +679,7 @@ class Account(object):
|
|
|
694
679
|
else:
|
|
695
680
|
file.write(info.name + "\n")
|
|
696
681
|
|
|
697
|
-
def get_symbol_type(
|
|
698
|
-
self, symbol: str
|
|
699
|
-
) -> Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "BOND", "unknown"]:
|
|
682
|
+
def get_symbol_type(self, symbol: str) -> SymbolType:
|
|
700
683
|
"""
|
|
701
684
|
Determines the type of a given financial instrument symbol.
|
|
702
685
|
|
|
@@ -704,41 +687,41 @@ class Account(object):
|
|
|
704
687
|
symbol (str): The symbol of the financial instrument (e.g., `GOOGL`, `EURUSD`).
|
|
705
688
|
|
|
706
689
|
Returns:
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
- `
|
|
711
|
-
- `
|
|
712
|
-
- `
|
|
713
|
-
- `
|
|
714
|
-
- `
|
|
715
|
-
- `
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
Returns `unknown` if the type cannot be determined.
|
|
690
|
+
SymbolType: The type of the financial instrument, one of the following:
|
|
691
|
+
- `SymbolType.ETFs`
|
|
692
|
+
- `SymbolType.BONDS`
|
|
693
|
+
- `SymbolType.FOREX`
|
|
694
|
+
- `SymbolType.FUTURES`
|
|
695
|
+
- `SymbolType.STOCKS`
|
|
696
|
+
- `SymbolType.INDICES`
|
|
697
|
+
- `SymbolType.COMMODITIES`
|
|
698
|
+
- `SymbolType.CRYPTO`
|
|
699
|
+
- `SymbolType.unknown` if the type cannot be determined.
|
|
700
|
+
|
|
720
701
|
"""
|
|
721
702
|
|
|
722
703
|
patterns = _SYMBOLS_TYPE_
|
|
723
704
|
info = self.get_symbol_info(symbol)
|
|
724
|
-
indices = self.get_symbols(symbol_type=
|
|
725
|
-
commodity = self.get_symbols(symbol_type=
|
|
705
|
+
indices = self.get_symbols(symbol_type=SymbolType.INDICES)
|
|
706
|
+
commodity = self.get_symbols(symbol_type=SymbolType.COMMODITIES)
|
|
726
707
|
if info is not None:
|
|
727
708
|
for symbol_type, pattern in patterns.items():
|
|
728
709
|
if (
|
|
729
|
-
symbol_type in [
|
|
710
|
+
symbol_type in [SymbolType.COMMODITIES, SymbolType.INDICES]
|
|
730
711
|
and self.broker == PepperstoneGroupLimited()
|
|
731
712
|
and info.name.endswith("-F")
|
|
732
713
|
and info.name in indices + commodity
|
|
733
714
|
):
|
|
734
|
-
symbol_type =
|
|
715
|
+
symbol_type = SymbolType.FUTURES
|
|
735
716
|
pattern = r"\b(Forwards?)\b"
|
|
736
717
|
search = re.compile(pattern)
|
|
737
718
|
if re.search(search, info.path):
|
|
738
719
|
return symbol_type
|
|
739
|
-
return
|
|
720
|
+
return SymbolType.unknown
|
|
740
721
|
|
|
741
|
-
def _get_symbols_by_category(
|
|
722
|
+
def _get_symbols_by_category(
|
|
723
|
+
self, symbol_type: SymbolType | str, category, category_map
|
|
724
|
+
):
|
|
742
725
|
if category not in category_map:
|
|
743
726
|
raise ValueError(
|
|
744
727
|
f"Unsupported category: {category}. Choose from: {', '.join(category_map)}"
|
|
@@ -777,8 +760,11 @@ class Account(object):
|
|
|
777
760
|
This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
|
|
778
761
|
For other brokers use `get_symbols()` or this method will use it by default.
|
|
779
762
|
"""
|
|
780
|
-
if
|
|
781
|
-
|
|
763
|
+
if (
|
|
764
|
+
self.broker != AdmiralMarktsGroup()
|
|
765
|
+
or self.broker != PepperstoneGroupLimited()
|
|
766
|
+
):
|
|
767
|
+
return self.get_symbols(symbol_type=SymbolType.FOREX)
|
|
782
768
|
else:
|
|
783
769
|
fx_categories = {
|
|
784
770
|
"majors": r"\b(Majors?)\b",
|
|
@@ -787,7 +773,9 @@ class Account(object):
|
|
|
787
773
|
"crosses": r"\b(Crosses?)\b",
|
|
788
774
|
"ndfs": r"\b(NDFs?)\b",
|
|
789
775
|
}
|
|
790
|
-
return self._get_symbols_by_category(
|
|
776
|
+
return self._get_symbols_by_category(
|
|
777
|
+
SymbolType.FOREX, category, fx_categories
|
|
778
|
+
)
|
|
791
779
|
|
|
792
780
|
def get_stocks_from_country(
|
|
793
781
|
self, country_code: str = "USA", etf=False
|
|
@@ -828,17 +816,22 @@ class Account(object):
|
|
|
828
816
|
This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
|
|
829
817
|
For other brokers use `get_symbols()` or this method will use it by default.
|
|
830
818
|
"""
|
|
831
|
-
|
|
832
|
-
if
|
|
833
|
-
|
|
834
|
-
|
|
819
|
+
|
|
820
|
+
if (
|
|
821
|
+
self.broker != AdmiralMarktsGroup()
|
|
822
|
+
or self.broker != PepperstoneGroupLimited()
|
|
823
|
+
):
|
|
824
|
+
return self.get_symbols(symbol_type=SymbolType.STOCKS)
|
|
835
825
|
else:
|
|
826
|
+
stocks, etfs = [], []
|
|
836
827
|
country_map = _COUNTRY_MAP_
|
|
837
|
-
stocks = self._get_symbols_by_category(
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
828
|
+
stocks = self._get_symbols_by_category(
|
|
829
|
+
SymbolType.STOCKS, country_code, country_map
|
|
830
|
+
)
|
|
831
|
+
etfs = self._get_symbols_by_category(
|
|
832
|
+
SymbolType.ETFs, country_code, country_map
|
|
833
|
+
)
|
|
834
|
+
return stocks + etfs if etf else stocks
|
|
842
835
|
|
|
843
836
|
def get_stocks_from_exchange(
|
|
844
837
|
self, exchange_code: str = "XNYS", etf=True
|
|
@@ -882,15 +875,17 @@ class Account(object):
|
|
|
882
875
|
For other brokers use `get_symbols()` or this method will use it by default.
|
|
883
876
|
"""
|
|
884
877
|
if self.broker != AdmiralMarktsGroup():
|
|
885
|
-
|
|
886
|
-
return stocks
|
|
878
|
+
return self.get_symbols(symbol_type=SymbolType.STOCKS)
|
|
887
879
|
else:
|
|
880
|
+
stocks, etfs = [], []
|
|
888
881
|
exchange_map = AMG_EXCHANGES
|
|
889
|
-
stocks = self._get_symbols_by_category(
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
882
|
+
stocks = self._get_symbols_by_category(
|
|
883
|
+
SymbolType.STOCKS, exchange_code, exchange_map
|
|
884
|
+
)
|
|
885
|
+
etfs = self._get_symbols_by_category(
|
|
886
|
+
SymbolType.ETFs, exchange_code, exchange_map
|
|
887
|
+
)
|
|
888
|
+
return stocks + etfs if etf else stocks
|
|
894
889
|
|
|
895
890
|
def get_future_symbols(self, category: str = "ALL") -> List[str]:
|
|
896
891
|
"""
|
|
@@ -913,48 +908,42 @@ class Account(object):
|
|
|
913
908
|
"""
|
|
914
909
|
category = category.lower()
|
|
915
910
|
if self.broker != AdmiralMarktsGroup():
|
|
916
|
-
return self.get_symbols(symbol_type=
|
|
911
|
+
return self.get_symbols(symbol_type=SymbolType.FUTURES)
|
|
917
912
|
elif category in ["all", "index"]:
|
|
918
913
|
categories = {
|
|
919
914
|
"all": r"\b(Futures?)\b",
|
|
920
915
|
"index": r"\b(Index)\b",
|
|
921
916
|
}
|
|
922
|
-
return self._get_symbols_by_category(
|
|
917
|
+
return self._get_symbols_by_category(
|
|
918
|
+
SymbolType.FUTURES, category, categories
|
|
919
|
+
)
|
|
923
920
|
else:
|
|
924
|
-
|
|
925
|
-
energies
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
futures = self.get_symbols(symbol_type="FUT")
|
|
921
|
+
futures_types = {}
|
|
922
|
+
for future_type in ("metals", "energies", "agricultures", "bonds"):
|
|
923
|
+
futures_types[future_type] = []
|
|
924
|
+
commodities = self.get_symbols(symbol_type=SymbolType.COMMODITIES)
|
|
925
|
+
futures = self.get_symbols(symbol_type=SymbolType.FUTURES)
|
|
930
926
|
for symbol in futures:
|
|
931
927
|
info = self.get_symbol_info(symbol)
|
|
932
928
|
if info.name.startswith("_"):
|
|
933
929
|
if "XAU" in info.name:
|
|
934
|
-
metals.append(info.name)
|
|
930
|
+
futures_types["metals"].append(info.name)
|
|
935
931
|
if "oil" in info.name.lower():
|
|
936
|
-
energies.append(info.name)
|
|
932
|
+
futures_types["energies"].append(info.name)
|
|
937
933
|
name = info.name.split("_")[1]
|
|
938
934
|
if name in commodities:
|
|
939
935
|
_info = self.get_symbol_info(name)
|
|
940
936
|
if "Metals" in _info.path:
|
|
941
|
-
metals.append(info.name)
|
|
937
|
+
futures_types["metals"].append(info.name)
|
|
942
938
|
elif "Energies" in _info.path:
|
|
943
|
-
energies.append(info.name)
|
|
939
|
+
futures_types["energies"].append(info.name)
|
|
944
940
|
elif "Agricultures" in _info.path:
|
|
945
|
-
agricultures.append(info.name)
|
|
941
|
+
futures_types["agricultures"].append(info.name)
|
|
946
942
|
|
|
947
943
|
elif info.name.startswith("#"):
|
|
948
944
|
if "Index" not in info.path:
|
|
949
|
-
bonds.append(info.name)
|
|
950
|
-
|
|
951
|
-
return metals
|
|
952
|
-
elif category == "energies":
|
|
953
|
-
return energies
|
|
954
|
-
elif category == "agricultures":
|
|
955
|
-
return agricultures
|
|
956
|
-
elif category == "bonds":
|
|
957
|
-
return bonds
|
|
945
|
+
futures_types["bonds"].append(info.name)
|
|
946
|
+
return futures_types[category]
|
|
958
947
|
|
|
959
948
|
def get_symbol_info(self, symbol: str) -> Union[SymbolInfo, None]:
|
|
960
949
|
"""Get symbol properties
|
|
@@ -978,12 +967,16 @@ class Account(object):
|
|
|
978
967
|
return None
|
|
979
968
|
else:
|
|
980
969
|
symbol_info_dict = symbol_info._asdict()
|
|
981
|
-
time =
|
|
970
|
+
time = (
|
|
971
|
+
datetime.fromtimestamp(symbol_info.time)
|
|
972
|
+
if isinstance(symbol_info.time, int)
|
|
973
|
+
else symbol_info.time
|
|
974
|
+
)
|
|
982
975
|
symbol_info_dict["time"] = time
|
|
983
976
|
return SymbolInfo(**symbol_info_dict)
|
|
984
977
|
except Exception as e:
|
|
985
978
|
msg = self._symbol_info_msg(symbol)
|
|
986
|
-
raise_mt5_error(message=f"{e
|
|
979
|
+
raise_mt5_error(message=f"{str(e)} {msg}")
|
|
987
980
|
|
|
988
981
|
def show_symbol_info(self, symbol: str):
|
|
989
982
|
"""
|
|
@@ -1024,12 +1017,16 @@ class Account(object):
|
|
|
1024
1017
|
return None
|
|
1025
1018
|
else:
|
|
1026
1019
|
info_dict = tick_info._asdict()
|
|
1027
|
-
time =
|
|
1020
|
+
time = (
|
|
1021
|
+
datetime.fromtimestamp(tick_info.time)
|
|
1022
|
+
if isinstance(tick_info.time, int)
|
|
1023
|
+
else tick_info.time
|
|
1024
|
+
)
|
|
1028
1025
|
info_dict["time"] = time
|
|
1029
1026
|
return TickInfo(**info_dict)
|
|
1030
1027
|
except Exception as e:
|
|
1031
1028
|
msg = self._symbol_info_msg(symbol)
|
|
1032
|
-
raise_mt5_error(message=f"{e
|
|
1029
|
+
raise_mt5_error(message=f"{str(e)} {msg}")
|
|
1033
1030
|
|
|
1034
1031
|
def show_tick_info(self, symbol: str):
|
|
1035
1032
|
"""
|
|
@@ -1050,16 +1047,17 @@ class Account(object):
|
|
|
1050
1047
|
Returns:
|
|
1051
1048
|
The Market Depth content as a tuple from BookInfo entries featuring order type, price and volume in lots.
|
|
1052
1049
|
Return None in case of an error.
|
|
1053
|
-
|
|
1050
|
+
|
|
1054
1051
|
Raises:
|
|
1055
1052
|
MT5TerminalError: A specific exception based on the error code.
|
|
1056
1053
|
"""
|
|
1057
1054
|
try:
|
|
1058
1055
|
book = mt5.market_book_get(symbol)
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1056
|
+
return (
|
|
1057
|
+
None
|
|
1058
|
+
if book is None
|
|
1059
|
+
else tuple([BookInfo(**entry._asdict()) for entry in book])
|
|
1060
|
+
)
|
|
1063
1061
|
except Exception as e:
|
|
1064
1062
|
raise_mt5_error(e)
|
|
1065
1063
|
|
|
@@ -1083,10 +1081,7 @@ class Account(object):
|
|
|
1083
1081
|
"""
|
|
1084
1082
|
actions = {"buy": mt5.ORDER_TYPE_BUY, "sell": mt5.ORDER_TYPE_SELL}
|
|
1085
1083
|
try:
|
|
1086
|
-
|
|
1087
|
-
if margin is None:
|
|
1088
|
-
return None
|
|
1089
|
-
return margin
|
|
1084
|
+
return mt5.order_calc_margin(actions[action], symbol, lot, price)
|
|
1090
1085
|
except Exception as e:
|
|
1091
1086
|
raise_mt5_error(e)
|
|
1092
1087
|
|
bbstrader/metatrader/copier.py
CHANGED
|
@@ -302,10 +302,9 @@ class TradeCopier(object):
|
|
|
302
302
|
if self.start_time is None or self.end_time is None:
|
|
303
303
|
return True
|
|
304
304
|
else:
|
|
305
|
-
now = datetime.now()
|
|
306
305
|
start_time = datetime.strptime(self.start_time, "%H:%M").time()
|
|
307
306
|
end_time = datetime.strptime(self.end_time, "%H:%M").time()
|
|
308
|
-
if start_time <= now.time() <= end_time:
|
|
307
|
+
if start_time <= datetime.now().time() <= end_time:
|
|
309
308
|
return True
|
|
310
309
|
return False
|
|
311
310
|
|
|
@@ -327,7 +326,7 @@ class TradeCopier(object):
|
|
|
327
326
|
dest_eqty=Account(**destination).get_account_info().margin_free,
|
|
328
327
|
)
|
|
329
328
|
|
|
330
|
-
trade_instance = Trade(symbol=symbol, **destination, max_risk=100.0)
|
|
329
|
+
trade_instance = Trade(symbol=symbol, **destination, max_risk=100.0, logger=None)
|
|
331
330
|
try:
|
|
332
331
|
action = action_type[trade.type]
|
|
333
332
|
except KeyError:
|
|
@@ -395,7 +394,7 @@ class TradeCopier(object):
|
|
|
395
394
|
|
|
396
395
|
def remove_order(self, src_symbol, order: TradeOrder, destination: dict):
|
|
397
396
|
check_mt5_connection(**destination)
|
|
398
|
-
trade = Trade(symbol=order.symbol, **destination)
|
|
397
|
+
trade = Trade(symbol=order.symbol, **destination, logger=None)
|
|
399
398
|
if trade.close_order(order.ticket, id=order.magic):
|
|
400
399
|
logger.info(
|
|
401
400
|
f"Close Order #{order.ticket} on @{destination.get('login')}::{order.symbol}, "
|
|
@@ -438,7 +437,7 @@ class TradeCopier(object):
|
|
|
438
437
|
|
|
439
438
|
def remove_position(self, src_symbol, position: TradePosition, destination: dict):
|
|
440
439
|
check_mt5_connection(**destination)
|
|
441
|
-
trade = Trade(symbol=position.symbol, **destination)
|
|
440
|
+
trade = Trade(symbol=position.symbol, **destination, logger=None)
|
|
442
441
|
if trade.close_position(position.ticket, id=position.magic):
|
|
443
442
|
logger.info(
|
|
444
443
|
f"Close Position #{position.ticket} on @{destination.get('login')}::{position.symbol}, "
|
|
@@ -626,7 +625,7 @@ class TradeCopier(object):
|
|
|
626
625
|
time.sleep(0.1)
|
|
627
626
|
except KeyboardInterrupt:
|
|
628
627
|
logger.info("Stopping the Trade Copier ...")
|
|
629
|
-
|
|
628
|
+
exit(0)
|
|
630
629
|
except Exception as e:
|
|
631
630
|
self.log_error(e)
|
|
632
631
|
time.sleep(self.sleeptime)
|