Add rate view resolution and downstream SDK helpers (#18)
* Add public helpers to resolve rate compatibility view names. Expose resolve_rate_view_name and resolve_rate_view_names in mt5cli.history so consumers can derive mt5cli-managed SQLite view names from stored rates metadata without reimplementing the naming rules. Co-authored-by: Cursor <cursoragent@cursor.com> * Add reusable export, tick-window, and margin helpers for downstream tools. Expose SQLite append/dedup export, recent tick retrieval, and minimum margin summary through the SDK and CLI so projects like mteor can depend on mt5cli instead of duplicating MT5 data plumbing. Co-authored-by: Cursor <cursoragent@cursor.com> * Bump version to 0.4.3. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback for rate view resolution and SDK helpers. Harden SQLite read-only connections, tighten view discovery, improve recent_ticks fetch efficiency, default SQLite export to append, and expand tests and docs. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix read-only SQLite URI construction on Windows. Use Path.as_uri() so encoded file URIs work cross-platform with mode=ro. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+171
-2
@@ -10,6 +10,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Self, TypeVar
|
||||
|
||||
import pandas as pd
|
||||
from pdmt5 import Mt5Config, Mt5DataClient
|
||||
|
||||
from .history import (
|
||||
@@ -33,8 +34,6 @@ from .utils import (
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
|
||||
import pandas as pd
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -53,8 +52,10 @@ __all__ = [
|
||||
"history_orders",
|
||||
"last_error",
|
||||
"market_book",
|
||||
"minimum_margins",
|
||||
"orders",
|
||||
"positions",
|
||||
"recent_ticks",
|
||||
"symbol_info",
|
||||
"symbol_info_tick",
|
||||
"symbols",
|
||||
@@ -89,6 +90,89 @@ def _coerce_datetime(value: datetime | str | None) -> datetime | None:
|
||||
return parse_datetime(value)
|
||||
|
||||
|
||||
def _coerce_tick_time(value: object) -> datetime:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return parse_datetime(value)
|
||||
if isinstance(value, (int, float)):
|
||||
return datetime.fromtimestamp(value, tz=UTC)
|
||||
msg = f"Unsupported tick time value: {value!r}"
|
||||
raise TypeError(msg)
|
||||
|
||||
|
||||
def _filter_ticks_to_end(frame: pd.DataFrame, end: datetime) -> pd.DataFrame:
|
||||
if frame.empty or "time" not in frame.columns:
|
||||
return frame
|
||||
times = pd.to_datetime(frame["time"], utc=True)
|
||||
return frame.loc[times <= end].reset_index(drop=True)
|
||||
|
||||
|
||||
def _fetch_recent_ticks(
|
||||
client: Mt5DataClient,
|
||||
symbol: str,
|
||||
seconds: float,
|
||||
date_to: datetime | None,
|
||||
count: int,
|
||||
flags: int,
|
||||
) -> pd.DataFrame:
|
||||
if date_to is not None:
|
||||
end = date_to
|
||||
else:
|
||||
tick = client.symbol_info_tick(symbol)
|
||||
end = _coerce_tick_time(tick.time)
|
||||
start = end - timedelta(seconds=seconds)
|
||||
if count > 0:
|
||||
from_frame = _filter_ticks_to_end(
|
||||
client.copy_ticks_from_as_df(
|
||||
symbol=symbol,
|
||||
date_from=start,
|
||||
count=count,
|
||||
flags=flags,
|
||||
),
|
||||
end,
|
||||
)
|
||||
if len(from_frame) < count:
|
||||
return from_frame
|
||||
frame = client.copy_ticks_range_as_df(
|
||||
symbol=symbol,
|
||||
date_from=start,
|
||||
date_to=end,
|
||||
flags=flags,
|
||||
)
|
||||
if count > 0 and len(frame) > count:
|
||||
return frame.tail(count).reset_index(drop=True)
|
||||
return frame
|
||||
|
||||
|
||||
def _fetch_minimum_margins(client: Mt5DataClient, symbol: str) -> pd.DataFrame:
|
||||
sym = client.symbol_info(symbol)
|
||||
account = client.account_info()
|
||||
tick = client.symbol_info_tick(symbol)
|
||||
volume_min = sym.volume_min
|
||||
buy_margin = client.order_calc_margin(
|
||||
client.mt5.ORDER_TYPE_BUY,
|
||||
symbol,
|
||||
volume_min,
|
||||
tick.ask,
|
||||
)
|
||||
sell_margin = client.order_calc_margin(
|
||||
client.mt5.ORDER_TYPE_SELL,
|
||||
symbol,
|
||||
volume_min,
|
||||
tick.bid,
|
||||
)
|
||||
return pd.DataFrame([
|
||||
{
|
||||
"symbol": symbol,
|
||||
"account_currency": account.currency,
|
||||
"volume_min": volume_min,
|
||||
"buy_margin": buy_margin,
|
||||
"sell_margin": sell_margin,
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
def build_config(
|
||||
*,
|
||||
path: str | None = None,
|
||||
@@ -418,6 +502,57 @@ class Mt5CliClient:
|
||||
"""Return market depth for a symbol."""
|
||||
return self._fetch(lambda c: c.market_book_get_as_df(symbol=symbol))
|
||||
|
||||
def recent_ticks(
|
||||
self,
|
||||
symbol: str,
|
||||
seconds: float,
|
||||
*,
|
||||
date_to: datetime | str | None = None,
|
||||
count: int = 10000,
|
||||
flags: int | str = "ALL",
|
||||
) -> pd.DataFrame:
|
||||
"""Return ticks from a recent time window.
|
||||
|
||||
Args:
|
||||
symbol: Symbol name.
|
||||
seconds: Lookback window in seconds ending at ``date_to``.
|
||||
date_to: Window end time. When ``None``, uses the latest
|
||||
``symbol_info_tick().time`` rather than wall-clock now.
|
||||
count: Maximum ticks to return. Values ``<= 0`` return the full
|
||||
window without trimming. Positive values keep the most recent
|
||||
ticks; when the window is sparse, ``copy_ticks_from`` avoids
|
||||
fetching the entire range.
|
||||
flags: Tick flags as ``ALL``, ``INFO``, ``TRADE``, or an integer.
|
||||
|
||||
Returns:
|
||||
Tick DataFrame with MT5 tick columns such as ``time``, ``bid``,
|
||||
``ask``, ``last``, and ``volume``.
|
||||
"""
|
||||
tick_flags = _coerce_tick_flags(flags)
|
||||
end = _coerce_datetime(date_to)
|
||||
return self._fetch(
|
||||
lambda c: _fetch_recent_ticks(
|
||||
c,
|
||||
symbol,
|
||||
seconds,
|
||||
end,
|
||||
count,
|
||||
tick_flags,
|
||||
),
|
||||
)
|
||||
|
||||
def minimum_margins(self, symbol: str) -> pd.DataFrame:
|
||||
"""Return minimum-volume buy and sell margin requirements.
|
||||
|
||||
Args:
|
||||
symbol: Symbol name.
|
||||
|
||||
Returns:
|
||||
One-row DataFrame with columns ``symbol``, ``account_currency``,
|
||||
``volume_min``, ``buy_margin``, and ``sell_margin``.
|
||||
"""
|
||||
return self._fetch(lambda c: _fetch_minimum_margins(c, symbol))
|
||||
|
||||
|
||||
def _resolve_incremental_settings(
|
||||
selected_datasets: set[Dataset],
|
||||
@@ -915,3 +1050,37 @@ def market_book(
|
||||
) -> pd.DataFrame:
|
||||
"""Return market depth for a symbol."""
|
||||
return _make_client(config=config).market_book(symbol)
|
||||
|
||||
|
||||
def recent_ticks(
|
||||
symbol: str,
|
||||
seconds: float,
|
||||
*,
|
||||
date_to: datetime | str | None = None,
|
||||
count: int = 10000,
|
||||
flags: int | str = "ALL",
|
||||
config: Mt5Config | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""Return ticks from a recent time window ending at ``date_to`` or now.
|
||||
|
||||
See ``Mt5CliClient.recent_ticks`` for parameter and return details.
|
||||
"""
|
||||
return _make_client(config=config).recent_ticks(
|
||||
symbol,
|
||||
seconds,
|
||||
date_to=date_to,
|
||||
count=count,
|
||||
flags=flags,
|
||||
)
|
||||
|
||||
|
||||
def minimum_margins(
|
||||
symbol: str,
|
||||
*,
|
||||
config: Mt5Config | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""Return minimum-volume buy and sell margin requirements.
|
||||
|
||||
See ``Mt5CliClient.minimum_margins`` for return details.
|
||||
"""
|
||||
return _make_client(config=config).minimum_margins(symbol)
|
||||
|
||||
Reference in New Issue
Block a user