Add generic MT5 rate-loading SDK APIs for downstream reuse (#20)

This commit is contained in:
Daichi Narushima
2026-06-09 15:37:24 +09:00
committed by GitHub
parent 9957b0a1de
commit 1b69e8f08e
7 changed files with 946 additions and 11 deletions
+138 -1
View File
@@ -6,7 +6,7 @@ import json
import logging
import sqlite3
from contextlib import contextmanager
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING, Self, TypeVar, cast
@@ -40,11 +40,13 @@ T = TypeVar("T")
logger = logging.getLogger(__name__)
__all__ = [
"AccountSpec",
"Mt5CliClient",
"account_info",
"build_config",
"collect_history",
"collect_latest_rates",
"collect_latest_rates_for_accounts",
"copy_rates_from",
"copy_rates_from_pos",
"copy_rates_range",
@@ -56,6 +58,7 @@ __all__ = [
"latest_rates",
"market_book",
"minimum_margins",
"mt5_session",
"mt5_summary",
"mt5_summary_as_df",
"orders",
@@ -277,6 +280,26 @@ def _run_with_client(
return fetch_fn(client)
@contextmanager
def mt5_session(config: Mt5Config | None = None) -> Iterator[Mt5CliClient]:
"""Open an MT5 terminal session and yield a connected client.
Launches the MetaTrader 5 terminal using ``Mt5Config.path`` (when set),
logs in, yields a connected :class:`Mt5CliClient`, and always shuts the
terminal down on exit.
Args:
config: MT5 connection configuration. Defaults to an empty config that
attaches to a running terminal.
Yields:
Connected ``Mt5CliClient`` bound to the session.
"""
mt5_config = config or build_config()
with _connected_client(mt5_config) as client:
yield Mt5CliClient.from_connected_client(client)
class Mt5CliClient:
"""Programmatic client for read-only MetaTrader 5 data access."""
@@ -1075,6 +1098,120 @@ def collect_latest_rates(
)
@dataclass(frozen=True)
class AccountSpec:
"""Connection parameters and symbols for one MT5 account group.
Attributes:
symbols: Symbols to load latest rates for under this account.
login: Trading account login. String values are coerced to int when
non-empty.
password: Trading account password.
server: Trading server name.
path: Path to the MetaTrader5 terminal EXE file.
timeout: Connection timeout in milliseconds.
"""
symbols: Sequence[str]
login: int | str | None = None
password: str | None = field(default=None, repr=False)
server: str | None = None
path: str | None = None
timeout: int | None = None
def _coerce_login(login: int | str | None) -> int | None:
"""Coerce a login value to int, treating empty strings as unset.
Returns:
Integer login, or None when unset or an empty string.
"""
if login is None or isinstance(login, int):
return login
text = login.strip()
if not text:
return None
return int(text)
def _build_account_config(
account: AccountSpec,
base_config: Mt5Config | None,
) -> Mt5Config:
"""Build an ``Mt5Config`` for an account, falling back to ``base_config``.
Returns:
Merged MT5 configuration for the account.
"""
login = _coerce_login(account.login)
if login is None and base_config is not None:
login = base_config.login
return build_config(
path=account.path or (base_config.path if base_config else None),
login=login,
password=account.password or (base_config.password if base_config else None),
server=account.server or (base_config.server if base_config else None),
timeout=account.timeout
if account.timeout is not None
else (base_config.timeout if base_config else None),
)
def collect_latest_rates_for_accounts(
accounts: Sequence[AccountSpec],
timeframes: Sequence[int | str],
count: int,
*,
start_pos: int = 0,
base_config: Mt5Config | None = None,
) -> dict[tuple[str, int], pd.DataFrame]:
"""Collect latest rates across multiple MT5 account groups.
Each account is connected in turn, its symbols are read for every
timeframe, and the resulting frames are merged into a single mapping.
Args:
accounts: Account groups to read. Each must define at least one symbol.
timeframes: MT5 timeframes as integers or names (for example ``M1``).
count: Number of most recent bars to read per symbol/timeframe.
start_pos: Initial bar position offset.
base_config: Optional base configuration whose fields fill any value not
set on an individual account.
Returns:
Mapping keyed by ``(symbol, timeframe_int)``. When accounts share a
symbol/timeframe pair, the last account processed wins.
Raises:
ValueError: If ``accounts``, ``timeframes``, or any account's symbols are
empty, or ``count`` is not positive.
"""
account_list = list(accounts)
if not account_list:
msg = "At least one account is required."
raise ValueError(msg)
if not timeframes:
msg = "At least one timeframe is required."
raise ValueError(msg)
if any(not account.symbols for account in account_list):
msg = "Each account requires at least one symbol."
raise ValueError(msg)
_require_positive(count, "count")
result: dict[tuple[str, int], pd.DataFrame] = {}
for account in account_list:
config = _build_account_config(account, base_config)
with Mt5CliClient(config=config) as client:
result.update(
client.collect_latest_rates(
account.symbols,
timeframes,
count=count,
start_pos=start_pos,
),
)
return result
def copy_rates_range(
symbol: str,
timeframe: int | str,