Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 513eb7617d |
@@ -45,7 +45,7 @@ These names are exported from `mt5cli` and enumerated in
|
|||||||
| `MT5Client` | Read-only data client with optional `order_check` / `order_send` |
|
| `MT5Client` | Read-only data client with optional `order_check` / `order_send` |
|
||||||
| `build_config` | Build `pdmt5.Mt5Config` from connection fields; `login` accepts `int \| str \| None` — numeric strings are coerced to `int`, blank strings are treated as unset, and `${ENV_VAR}` / `$ENV_NAME` placeholders in string parameters are expanded when `allow_whole_dollar_env=True` |
|
| `build_config` | Build `pdmt5.Mt5Config` from connection fields; `login` accepts `int \| str \| None` — numeric strings are coerced to `int`, blank strings are treated as unset, and `${ENV_VAR}` / `$ENV_NAME` placeholders in string parameters are expanded when `allow_whole_dollar_env=True` |
|
||||||
| `mt5_session` | Context manager: initialize, login, yield client, shutdown |
|
| `mt5_session` | Context manager: initialize, login, yield client, shutdown |
|
||||||
| `create_trading_client`, `mt5_trading_session` | Trading-capable MT5 client lifecycle; returns a client supporting order execution and account management |
|
| `create_trading_client`, `mt5_trading_session` | Trading-capable MT5 client lifecycle; returns a raw `pdmt5.Mt5DataClient` (not `MT5Client`) supporting order execution, account management, and history deal retrieval |
|
||||||
| `AccountSpec` | Generic account group: symbols plus optional credentials |
|
| `AccountSpec` | Generic account group: symbols plus optional credentials |
|
||||||
| `resolve_account_spec`, `resolve_account_specs` | Merge overrides and expand `${ENV_VAR}` placeholders; opt-in `allow_whole_dollar_env` for bare `$NAME` |
|
| `resolve_account_spec`, `resolve_account_specs` | Merge overrides and expand `${ENV_VAR}` placeholders; opt-in `allow_whole_dollar_env` for bare `$NAME` |
|
||||||
|
|
||||||
@@ -99,6 +99,7 @@ strategy entries, exits, Kelly sizing, or signal logic.
|
|||||||
| `determine_order_limits` | SL/TP price levels from ratios |
|
| `determine_order_limits` | SL/TP price levels from ratios |
|
||||||
| `calculate_trailing_stop_updates` | Per-ticket generic trailing stop-loss update plan |
|
| `calculate_trailing_stop_updates` | Per-ticket generic trailing stop-loss update plan |
|
||||||
| `ensure_symbol_selected` | Select/verify Market Watch visibility |
|
| `ensure_symbol_selected` | Select/verify Market Watch visibility |
|
||||||
|
| `fetch_recent_history_deals_for_trading_client` | Recent deal history from a connected trading client |
|
||||||
| `place_market_order`, `close_open_positions`, `update_sltp_for_open_positions`, `update_trailing_stop_loss_for_open_positions` | Order execution helpers (`dry_run` supported) |
|
| `place_market_order`, `close_open_positions`, `update_sltp_for_open_positions`, `update_trailing_stop_loss_for_open_positions` | Order execution helpers (`dry_run` supported) |
|
||||||
| `MarginVolume`, `OrderLimits`, `OrderExecutionResult` | Typed return contracts for order helpers |
|
| `MarginVolume`, `OrderLimits`, `OrderExecutionResult` | Typed return contracts for order helpers |
|
||||||
| `OrderSide`, `OrderFillingMode`, `OrderTimeMode`, `PositionSide`, `ExecutionStatus` | Typed enums for order helpers |
|
| `OrderSide`, `OrderFillingMode`, `OrderTimeMode`, `PositionSide`, `ExecutionStatus` | Typed enums for order helpers |
|
||||||
@@ -254,6 +255,9 @@ The following belong in consuming applications, not in mt5cli:
|
|||||||
- Backtesting, walk-forward analysis, or parameter optimization
|
- Backtesting, walk-forward analysis, or parameter optimization
|
||||||
- Strategy-specific risk policy, position sizing systems, or Kelly fractions
|
- Strategy-specific risk policy, position sizing systems, or Kelly fractions
|
||||||
- Entry/exit decision logic or YAML strategy semantics
|
- Entry/exit decision logic or YAML strategy semantics
|
||||||
|
- Entry-deal classification, Kelly fractions, or betting-specific deal transformations
|
||||||
|
(use `fetch_recent_history_deals_for_trading_client` to retrieve raw deal data, then
|
||||||
|
apply downstream transformations in your own adapter layer)
|
||||||
- Application-specific credential schema keys wired into mt5cli internals
|
- Application-specific credential schema keys wired into mt5cli internals
|
||||||
|
|
||||||
mt5cli provides connection lifecycle, normalized data access, SQLite history
|
mt5cli provides connection lifecycle, normalized data access, SQLite history
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ client supporting order execution and account management, use `Mt5Config.path`
|
|||||||
to launch the terminal when configured, and `mt5_trading_session()` always
|
to launch the terminal when configured, and `mt5_trading_session()` always
|
||||||
calls `shutdown()` on exit.
|
calls `shutdown()` on exit.
|
||||||
|
|
||||||
|
`create_trading_client()` returns a raw `pdmt5.Mt5DataClient` instance, not the
|
||||||
|
higher-level `MT5Client` wrapper. Use `mt5_session()` / `MT5Client` for
|
||||||
|
read-only data collection; use `mt5_trading_session()` only where order
|
||||||
|
placement or trading calculations are required.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from mt5cli import create_trading_client, mt5_trading_session
|
from mt5cli import create_trading_client, mt5_trading_session
|
||||||
|
|
||||||
@@ -182,6 +187,55 @@ updates: list[OrderExecutionResult] = update_sltp_for_open_positions(
|
|||||||
Closes issue #33: strategy-neutral order planning and execution helpers exposed
|
Closes issue #33: strategy-neutral order planning and execution helpers exposed
|
||||||
through the stable package root without embedding entry/exit policy.
|
through the stable package root without embedding entry/exit policy.
|
||||||
|
|
||||||
|
## Retrieving recent history deals
|
||||||
|
|
||||||
|
`fetch_recent_history_deals_for_trading_client()` fetches history deals from an
|
||||||
|
already-connected trading client over a trailing time window. It works directly
|
||||||
|
with the object returned by `create_trading_client()` (a raw
|
||||||
|
`pdmt5.Mt5DataClient`) without requiring any additional wrapping.
|
||||||
|
|
||||||
|
The helper returns a chronologically sorted DataFrame with a `RangeIndex` and
|
||||||
|
all columns from the underlying client (`time`, `symbol`, `type`, `entry`,
|
||||||
|
`volume`, `profit`, `position_id`, etc.). It does **not** apply any
|
||||||
|
strategy-specific transformations — entry/exit classification, Kelly fractions,
|
||||||
|
and betting semantics belong in downstream applications.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mt5cli import (
|
||||||
|
create_trading_client,
|
||||||
|
fetch_recent_history_deals_for_trading_client,
|
||||||
|
)
|
||||||
|
|
||||||
|
client = create_trading_client(login=12345, server="Broker-Demo")
|
||||||
|
try:
|
||||||
|
deals_df = fetch_recent_history_deals_for_trading_client(
|
||||||
|
client,
|
||||||
|
symbol="JP225",
|
||||||
|
hours=24,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
client.shutdown()
|
||||||
|
```
|
||||||
|
|
||||||
|
Or inside a managed session:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mt5cli import fetch_recent_history_deals_for_trading_client, mt5_trading_session
|
||||||
|
|
||||||
|
with mt5_trading_session(login=12345, server="Broker-Demo") as client:
|
||||||
|
deals_df = fetch_recent_history_deals_for_trading_client(
|
||||||
|
client,
|
||||||
|
symbol="JP225",
|
||||||
|
hours=48,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`hours` must be positive; `date_to` defaults to `datetime.now(UTC)`. An empty
|
||||||
|
or `None` result from the underlying client is normalized to an empty DataFrame.
|
||||||
|
|
||||||
|
Downstream packages own all strategy-specific transformations. mt5cli does not
|
||||||
|
provide entry-deal classification, Kelly sizing, or any betting-specific helpers.
|
||||||
|
|
||||||
## Migration from application-local helpers
|
## Migration from application-local helpers
|
||||||
|
|
||||||
| Application-local concern | mt5cli replacement |
|
| Application-local concern | mt5cli replacement |
|
||||||
@@ -192,6 +246,7 @@ through the stable package root without embedding entry/exit policy.
|
|||||||
| Local broker volume step normalization | `normalize_order_volume()` |
|
| Local broker volume step normalization | `normalize_order_volume()` |
|
||||||
| Local order or position margin estimation | `estimate_order_margin()`, `calculate_positions_margin()` |
|
| Local order or position margin estimation | `estimate_order_margin()`, `calculate_positions_margin()` |
|
||||||
| Local closed-bar fetch from a trading session | `fetch_latest_closed_rates_for_trading_client()`, `fetch_latest_closed_rates_indexed()` |
|
| Local closed-bar fetch from a trading session | `fetch_latest_closed_rates_for_trading_client()`, `fetch_latest_closed_rates_indexed()` |
|
||||||
|
| Local recent deal history fetch from a trading session | `fetch_recent_history_deals_for_trading_client()` |
|
||||||
| Local SL/TP price derivation | `determine_order_limits()` |
|
| Local SL/TP price derivation | `determine_order_limits()` |
|
||||||
| Throttled SQLite history loop with ad-hoc error handling | `ThrottledHistoryUpdater(suppress_errors=True)` |
|
| Throttled SQLite history loop with ad-hoc error handling | `ThrottledHistoryUpdater(suppress_errors=True)` |
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ from .trading import (
|
|||||||
extract_tick_price,
|
extract_tick_price,
|
||||||
fetch_latest_closed_rates_for_trading_client,
|
fetch_latest_closed_rates_for_trading_client,
|
||||||
fetch_latest_closed_rates_indexed,
|
fetch_latest_closed_rates_indexed,
|
||||||
|
fetch_recent_history_deals_for_trading_client,
|
||||||
get_account_snapshot,
|
get_account_snapshot,
|
||||||
get_positions_frame,
|
get_positions_frame,
|
||||||
get_symbol_snapshot,
|
get_symbol_snapshot,
|
||||||
@@ -128,6 +129,7 @@ __all__ = [
|
|||||||
"fetch_latest_closed_rates",
|
"fetch_latest_closed_rates",
|
||||||
"fetch_latest_closed_rates_for_trading_client",
|
"fetch_latest_closed_rates_for_trading_client",
|
||||||
"fetch_latest_closed_rates_indexed",
|
"fetch_latest_closed_rates_indexed",
|
||||||
|
"fetch_recent_history_deals_for_trading_client",
|
||||||
"get_account_snapshot",
|
"get_account_snapshot",
|
||||||
"get_positions_frame",
|
"get_positions_frame",
|
||||||
"get_symbol_snapshot",
|
"get_symbol_snapshot",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ STABLE_SDK_EXPORTS: frozenset[str] = frozenset({
|
|||||||
"fetch_latest_closed_rates",
|
"fetch_latest_closed_rates",
|
||||||
"fetch_latest_closed_rates_for_trading_client",
|
"fetch_latest_closed_rates_for_trading_client",
|
||||||
"fetch_latest_closed_rates_indexed",
|
"fetch_latest_closed_rates_indexed",
|
||||||
|
"fetch_recent_history_deals_for_trading_client",
|
||||||
"get_account_snapshot",
|
"get_account_snapshot",
|
||||||
"get_positions_frame",
|
"get_positions_frame",
|
||||||
"get_symbol_snapshot",
|
"get_symbol_snapshot",
|
||||||
|
|||||||
+136
-3
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
from math import floor, isfinite
|
from math import floor, isfinite
|
||||||
from numbers import Integral, Real
|
from numbers import Integral, Real
|
||||||
from typing import TYPE_CHECKING, Literal, Protocol, TypedDict, cast
|
from typing import TYPE_CHECKING, Literal, Protocol, TypedDict, cast
|
||||||
@@ -80,6 +81,43 @@ class _Mt5ClientProtocol(Protocol):
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class _HistoryDealsClientProtocol(Protocol):
|
||||||
|
"""Minimal protocol for MT5 clients capable of retrieving history deals.
|
||||||
|
|
||||||
|
Describes the single method required by
|
||||||
|
:func:`fetch_recent_history_deals_for_trading_client`. The raw
|
||||||
|
``pdmt5.Mt5DataClient`` returned by :func:`create_trading_client`
|
||||||
|
satisfies this protocol. ``mt5cli.sdk.Mt5CliClient`` (used via
|
||||||
|
``mt5_session()``) exposes ``history_deals()`` instead and does not
|
||||||
|
satisfy this protocol.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def history_deals_get_as_df(
|
||||||
|
self,
|
||||||
|
date_from: datetime,
|
||||||
|
date_to: datetime,
|
||||||
|
group: str | None = None,
|
||||||
|
symbol: str | None = None,
|
||||||
|
ticket: int | None = None,
|
||||||
|
position: int | None = None,
|
||||||
|
) -> pd.DataFrame | None:
|
||||||
|
"""Return historical deals as a DataFrame, or None when none exist."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class _TradingHistoryDealsClientProtocol(
|
||||||
|
_Mt5ClientProtocol,
|
||||||
|
_HistoryDealsClientProtocol,
|
||||||
|
Protocol,
|
||||||
|
):
|
||||||
|
"""Combined protocol for trading clients that also support history deal retrieval.
|
||||||
|
|
||||||
|
The raw ``pdmt5.Mt5DataClient`` returned by :func:`create_trading_client`
|
||||||
|
satisfies both :class:`_Mt5ClientProtocol` and
|
||||||
|
:class:`_HistoryDealsClientProtocol`, so it satisfies this combined protocol.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
PositionSide = Literal["long", "short"]
|
PositionSide = Literal["long", "short"]
|
||||||
OrderSide = Literal["BUY", "SELL"]
|
OrderSide = Literal["BUY", "SELL"]
|
||||||
OrderFillingMode = Literal["IOC", "FOK", "RETURN"]
|
OrderFillingMode = Literal["IOC", "FOK", "RETURN"]
|
||||||
@@ -209,6 +247,7 @@ __all__ = [
|
|||||||
"extract_tick_price",
|
"extract_tick_price",
|
||||||
"fetch_latest_closed_rates_for_trading_client",
|
"fetch_latest_closed_rates_for_trading_client",
|
||||||
"fetch_latest_closed_rates_indexed",
|
"fetch_latest_closed_rates_indexed",
|
||||||
|
"fetch_recent_history_deals_for_trading_client",
|
||||||
"get_account_snapshot",
|
"get_account_snapshot",
|
||||||
"get_positions_frame",
|
"get_positions_frame",
|
||||||
"get_symbol_snapshot",
|
"get_symbol_snapshot",
|
||||||
@@ -579,11 +618,25 @@ def create_trading_client(
|
|||||||
path: str | None = None,
|
path: str | None = None,
|
||||||
timeout: int | None = None,
|
timeout: int | None = None,
|
||||||
retry_count: int = 0,
|
retry_count: int = 0,
|
||||||
) -> _Mt5ClientProtocol:
|
) -> _TradingHistoryDealsClientProtocol:
|
||||||
"""Return an initialized and logged-in trading client.
|
"""Return an initialized and logged-in trading client.
|
||||||
|
|
||||||
|
The returned object is a raw ``pdmt5.Mt5DataClient`` instance, not the
|
||||||
|
higher-level ``mt5cli.MT5Client`` wrapper. Use ``mt5_session()`` /
|
||||||
|
``MT5Client`` for read-only data collection. For live trading helpers
|
||||||
|
(margin, volume, order execution, position management) pass the returned
|
||||||
|
client to the strategy-agnostic helpers in this module.
|
||||||
|
|
||||||
|
For history deal retrieval use
|
||||||
|
:func:`fetch_recent_history_deals_for_trading_client`; the returned client
|
||||||
|
satisfies :class:`_HistoryDealsClientProtocol` so no additional wrapping is
|
||||||
|
required.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A client instance supporting the required MT5 trading methods.
|
A ``pdmt5.Mt5DataClient`` instance satisfying ``_Mt5ClientProtocol``
|
||||||
|
and ``_HistoryDealsClientProtocol``. Caller is responsible for calling
|
||||||
|
``client.shutdown()`` when done; prefer ``mt5_trading_session()`` to
|
||||||
|
manage lifetime automatically.
|
||||||
"""
|
"""
|
||||||
mt5_config = _resolve_config(
|
mt5_config = _resolve_config(
|
||||||
config=config,
|
config=config,
|
||||||
@@ -1744,6 +1797,86 @@ def fetch_latest_closed_rates_indexed(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_recent_history_deals_for_trading_client(
|
||||||
|
client: _HistoryDealsClientProtocol,
|
||||||
|
*,
|
||||||
|
symbol: str | None = None,
|
||||||
|
group: str | None = None,
|
||||||
|
hours: float = 24.0,
|
||||||
|
date_to: datetime | None = None,
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""Fetch recent history deals from an already-connected trading client.
|
||||||
|
|
||||||
|
Computes a trailing window ending at ``date_to`` (or ``datetime.now(UTC)``
|
||||||
|
when omitted) and delegates to the client's ``history_deals_get_as_df``
|
||||||
|
method. The object returned by :func:`create_trading_client` (a raw
|
||||||
|
``pdmt5.Mt5DataClient``) satisfies this protocol directly. Note that
|
||||||
|
``mt5cli.sdk.Mt5CliClient`` (used via ``mt5_session()``) exposes
|
||||||
|
``history_deals()``, not ``history_deals_get_as_df()``, and therefore does
|
||||||
|
not satisfy this protocol; use this helper with trading-client sessions only.
|
||||||
|
|
||||||
|
The returned DataFrame preserves every column from the underlying client
|
||||||
|
(``time``, ``symbol``, ``type``, ``entry``, ``volume``, ``profit``,
|
||||||
|
``position_id``, etc.). No strategy-specific transformations are applied;
|
||||||
|
downstream packages own entry/exit classification, Kelly fractions, and
|
||||||
|
any other betting or signal semantics.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Connected ``pdmt5.Mt5DataClient`` (or compatible) with
|
||||||
|
``history_deals_get_as_df`` capability, as returned by
|
||||||
|
:func:`create_trading_client`.
|
||||||
|
symbol: Optional symbol filter passed to the underlying client.
|
||||||
|
group: Optional symbol group filter passed to the underlying client.
|
||||||
|
hours: Trailing window length in hours. Must be positive.
|
||||||
|
date_to: Window end timestamp. Defaults to ``datetime.now(UTC)``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame ordered chronologically by ``time`` (when the column
|
||||||
|
exists) with a ``RangeIndex``. Schema-preserving empty DataFrames
|
||||||
|
(zero rows but columns present) are passed through with a reset
|
||||||
|
index. Returns a bare empty DataFrame only when the underlying
|
||||||
|
client returns ``None``.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If ``hours`` is not positive.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
from mt5cli import (
|
||||||
|
create_trading_client,
|
||||||
|
fetch_recent_history_deals_for_trading_client,
|
||||||
|
)
|
||||||
|
|
||||||
|
client = create_trading_client(login=12345, server="Broker-Demo")
|
||||||
|
try:
|
||||||
|
deals_df = fetch_recent_history_deals_for_trading_client(
|
||||||
|
client,
|
||||||
|
symbol="JP225",
|
||||||
|
hours=24,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
client.shutdown()
|
||||||
|
"""
|
||||||
|
if not isfinite(hours) or hours <= 0:
|
||||||
|
msg = "hours must be finite and positive."
|
||||||
|
raise ValueError(msg)
|
||||||
|
end = date_to if date_to is not None else datetime.now(UTC)
|
||||||
|
start = end - timedelta(hours=hours)
|
||||||
|
raw = client.history_deals_get_as_df(
|
||||||
|
date_from=start,
|
||||||
|
date_to=end,
|
||||||
|
group=group,
|
||||||
|
symbol=symbol,
|
||||||
|
)
|
||||||
|
if raw is None:
|
||||||
|
return pd.DataFrame()
|
||||||
|
if raw.empty:
|
||||||
|
return raw.reset_index(drop=True)
|
||||||
|
if "time" in raw.columns:
|
||||||
|
raw = raw.sort_values("time")
|
||||||
|
return raw.reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def mt5_trading_session(
|
def mt5_trading_session(
|
||||||
config: Mt5Config | None = None,
|
config: Mt5Config | None = None,
|
||||||
@@ -1754,7 +1887,7 @@ def mt5_trading_session(
|
|||||||
path: str | None = None,
|
path: str | None = None,
|
||||||
timeout: int | None = None,
|
timeout: int | None = None,
|
||||||
retry_count: int = 0,
|
retry_count: int = 0,
|
||||||
) -> Iterator[_Mt5ClientProtocol]:
|
) -> Iterator[_TradingHistoryDealsClientProtocol]:
|
||||||
"""Open a trading-capable MT5 session and always shut down safely.
|
"""Open a trading-capable MT5 session and always shut down safely.
|
||||||
|
|
||||||
Launches the MetaTrader 5 terminal using ``Mt5Config.path`` when set,
|
Launches the MetaTrader 5 terminal using ``Mt5Config.path`` when set,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mt5cli"
|
name = "mt5cli"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
description = "Generic MT5 data and execution infrastructure for Python applications"
|
description = "Generic MT5 data and execution infrastructure for Python applications"
|
||||||
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
||||||
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
||||||
|
|||||||
+41
-145
@@ -72,35 +72,30 @@ class TestExecuteExport:
|
|||||||
class TestCommands:
|
class TestCommands:
|
||||||
"""Tests for all CLI subcommands via CliRunner."""
|
"""Tests for all CLI subcommands via CliRunner."""
|
||||||
|
|
||||||
def test_account_info(
|
@pytest.mark.parametrize(
|
||||||
|
("command", "method"),
|
||||||
|
[
|
||||||
|
("account-info", "account_info_as_df"),
|
||||||
|
("terminal-info", "terminal_info_as_df"),
|
||||||
|
("positions", "positions_get_as_df"),
|
||||||
|
("version", "version_as_df"),
|
||||||
|
("last-error", "last_error_as_df"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_simple_command(
|
||||||
self,
|
self,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
mock_client: MagicMock,
|
mock_client: MagicMock,
|
||||||
|
command: str,
|
||||||
|
method: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test account-info command."""
|
"""Simple no-arg commands invoke the expected client method."""
|
||||||
output = tmp_path / "out.csv"
|
output = tmp_path / "out.csv"
|
||||||
result = runner.invoke(
|
result = runner.invoke(app, ["-o", str(output), command])
|
||||||
app,
|
|
||||||
["-o", str(output), "account-info"],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
mock_client.account_info_as_df.assert_called_once()
|
getattr(mock_client, method).assert_called_once()
|
||||||
assert output.exists()
|
assert output.exists()
|
||||||
|
|
||||||
def test_terminal_info(
|
|
||||||
self,
|
|
||||||
tmp_path: Path,
|
|
||||||
mock_client: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test terminal-info command."""
|
|
||||||
output = tmp_path / "out.csv"
|
|
||||||
result = runner.invoke(
|
|
||||||
app,
|
|
||||||
["-o", str(output), "terminal-info"],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
mock_client.terminal_info_as_df.assert_called_once()
|
|
||||||
|
|
||||||
def test_symbols(
|
def test_symbols(
|
||||||
self,
|
self,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
@@ -398,20 +393,6 @@ class TestCommands:
|
|||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
mock_client.orders_get_as_df.assert_called_once()
|
mock_client.orders_get_as_df.assert_called_once()
|
||||||
|
|
||||||
def test_positions(
|
|
||||||
self,
|
|
||||||
tmp_path: Path,
|
|
||||||
mock_client: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test positions command."""
|
|
||||||
output = tmp_path / "out.csv"
|
|
||||||
result = runner.invoke(
|
|
||||||
app,
|
|
||||||
["-o", str(output), "positions"],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
mock_client.positions_get_as_df.assert_called_once()
|
|
||||||
|
|
||||||
def test_history_orders(
|
def test_history_orders(
|
||||||
self,
|
self,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
@@ -532,28 +513,6 @@ class TestCommands:
|
|||||||
"symbols_total": 42,
|
"symbols_total": 42,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_version(
|
|
||||||
self,
|
|
||||||
tmp_path: Path,
|
|
||||||
mock_client: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test version command."""
|
|
||||||
output = tmp_path / "out.csv"
|
|
||||||
result = runner.invoke(app, ["-o", str(output), "version"])
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
mock_client.version_as_df.assert_called_once()
|
|
||||||
|
|
||||||
def test_last_error(
|
|
||||||
self,
|
|
||||||
tmp_path: Path,
|
|
||||||
mock_client: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test last-error command."""
|
|
||||||
output = tmp_path / "out.csv"
|
|
||||||
result = runner.invoke(app, ["-o", str(output), "last-error"])
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
mock_client.last_error_as_df.assert_called_once()
|
|
||||||
|
|
||||||
def test_symbol_info_tick(
|
def test_symbol_info_tick(
|
||||||
self,
|
self,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
@@ -755,38 +714,19 @@ class TestHelpText:
|
|||||||
output = normalize_cli_output(result.output)
|
output = normalize_cli_output(result.output)
|
||||||
assert "execution" in output.lower()
|
assert "execution" in output.lower()
|
||||||
|
|
||||||
def test_top_level_help_has_execution_panel(self) -> None:
|
@pytest.mark.parametrize("panel", ["Execution", "Data / Export"])
|
||||||
"""Top-level help must show an Execution command group."""
|
def test_top_level_help_has_panel(self, panel: str) -> None:
|
||||||
|
"""Top-level help must show all command group panels."""
|
||||||
result = runner.invoke(app, ["--help"])
|
result = runner.invoke(app, ["--help"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Execution" in result.output
|
assert panel in result.output
|
||||||
|
|
||||||
def test_top_level_help_has_data_export_panel(self) -> None:
|
@pytest.mark.parametrize("keyword", ["raw", "expert", "live"])
|
||||||
"""Top-level help must show a Data / Export command group."""
|
def test_order_send_help_keywords(self, keyword: str) -> None:
|
||||||
result = runner.invoke(app, ["--help"])
|
"""order-send help must mention raw, expert, and live."""
|
||||||
|
result = runner.invoke(app, ["-o", "out.csv", "order-send", "--help"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Data / Export" in result.output
|
assert keyword in normalize_cli_output(result.output).lower()
|
||||||
|
|
||||||
def test_order_send_help_mentions_expert_and_raw(self) -> None:
|
|
||||||
"""order-send help must communicate it is the expert raw-request path."""
|
|
||||||
result2 = runner.invoke(
|
|
||||||
app,
|
|
||||||
["-o", "out.csv", "order-send", "--help"],
|
|
||||||
)
|
|
||||||
assert result2.exit_code == 0
|
|
||||||
output = normalize_cli_output(result2.output)
|
|
||||||
assert "raw" in output.lower()
|
|
||||||
assert "expert" in output.lower()
|
|
||||||
|
|
||||||
def test_order_send_help_mentions_live_execution(self) -> None:
|
|
||||||
"""order-send help must warn about live execution."""
|
|
||||||
result = runner.invoke(
|
|
||||||
app,
|
|
||||||
["-o", "out.csv", "order-send", "--help"],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0
|
|
||||||
output = normalize_cli_output(result.output)
|
|
||||||
assert "live" in output.lower()
|
|
||||||
|
|
||||||
def test_close_positions_help_mentions_dry_run_and_yes(self) -> None:
|
def test_close_positions_help_mentions_dry_run_and_yes(self) -> None:
|
||||||
"""close-positions help must document both safety gates."""
|
"""close-positions help must document both safety gates."""
|
||||||
@@ -1952,75 +1892,31 @@ class TestSnapshotCommand:
|
|||||||
kwargs = updater.call_args.kwargs
|
kwargs = updater.call_args.kwargs
|
||||||
assert kwargs["symbols"] == ["EURUSD", "GBPUSD"]
|
assert kwargs["symbols"] == ["EURUSD", "GBPUSD"]
|
||||||
|
|
||||||
def test_snapshot_with_no_account_flag(
|
@pytest.mark.parametrize(
|
||||||
|
("flag", "kwarg"),
|
||||||
|
[
|
||||||
|
("--no-account", "include_account"),
|
||||||
|
("--no-positions", "include_positions"),
|
||||||
|
("--no-orders", "include_orders"),
|
||||||
|
("--no-terminal", "include_terminal"),
|
||||||
|
("--no-grafana-schema", "with_grafana_schema"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_snapshot_with_no_flag(
|
||||||
self,
|
self,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
|
flag: str,
|
||||||
|
kwarg: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""--no-account disables account snapshotting."""
|
"""Snapshot --no-X flags disable the corresponding snapshot component."""
|
||||||
updater = mocker.patch("mt5cli.cli.sdk.update_observability_with_config")
|
updater = mocker.patch("mt5cli.cli.sdk.update_observability_with_config")
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
app,
|
app,
|
||||||
["-o", str(tmp_path / "out.db"), "snapshot", "--no-account"],
|
["-o", str(tmp_path / "out.db"), "snapshot", flag],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert updater.call_args.kwargs["include_account"] is False
|
assert updater.call_args.kwargs[kwarg] is False
|
||||||
|
|
||||||
def test_snapshot_with_no_positions_flag(
|
|
||||||
self,
|
|
||||||
tmp_path: Path,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
) -> None:
|
|
||||||
"""--no-positions disables position snapshotting."""
|
|
||||||
updater = mocker.patch("mt5cli.cli.sdk.update_observability_with_config")
|
|
||||||
result = runner.invoke(
|
|
||||||
app,
|
|
||||||
["-o", str(tmp_path / "out.db"), "snapshot", "--no-positions"],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
assert updater.call_args.kwargs["include_positions"] is False
|
|
||||||
|
|
||||||
def test_snapshot_with_no_orders_flag(
|
|
||||||
self,
|
|
||||||
tmp_path: Path,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
) -> None:
|
|
||||||
"""--no-orders disables order snapshotting."""
|
|
||||||
updater = mocker.patch("mt5cli.cli.sdk.update_observability_with_config")
|
|
||||||
result = runner.invoke(
|
|
||||||
app,
|
|
||||||
["-o", str(tmp_path / "out.db"), "snapshot", "--no-orders"],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
assert updater.call_args.kwargs["include_orders"] is False
|
|
||||||
|
|
||||||
def test_snapshot_with_no_terminal_flag(
|
|
||||||
self,
|
|
||||||
tmp_path: Path,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
) -> None:
|
|
||||||
"""--no-terminal disables terminal snapshotting."""
|
|
||||||
updater = mocker.patch("mt5cli.cli.sdk.update_observability_with_config")
|
|
||||||
result = runner.invoke(
|
|
||||||
app,
|
|
||||||
["-o", str(tmp_path / "out.db"), "snapshot", "--no-terminal"],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
assert updater.call_args.kwargs["include_terminal"] is False
|
|
||||||
|
|
||||||
def test_snapshot_with_no_grafana_schema_flag(
|
|
||||||
self,
|
|
||||||
tmp_path: Path,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
) -> None:
|
|
||||||
"""--no-grafana-schema disables Grafana schema creation."""
|
|
||||||
updater = mocker.patch("mt5cli.cli.sdk.update_observability_with_config")
|
|
||||||
result = runner.invoke(
|
|
||||||
app,
|
|
||||||
["-o", str(tmp_path / "out.db"), "snapshot", "--no-grafana-schema"],
|
|
||||||
)
|
|
||||||
assert result.exit_code == 0, result.output
|
|
||||||
assert updater.call_args.kwargs["with_grafana_schema"] is False
|
|
||||||
|
|
||||||
def test_snapshot_with_publish_copy(
|
def test_snapshot_with_publish_copy(
|
||||||
self,
|
self,
|
||||||
|
|||||||
+15
-43
@@ -2861,57 +2861,29 @@ class TestUpdateObservability:
|
|||||||
)
|
)
|
||||||
spy.assert_called_once()
|
spy.assert_called_once()
|
||||||
|
|
||||||
def test_update_observability_skips_account_when_disabled(
|
@pytest.mark.parametrize(
|
||||||
|
("kwarg", "method"),
|
||||||
|
[
|
||||||
|
("include_account", "account_info_as_df"),
|
||||||
|
("include_positions", "positions_get_as_df"),
|
||||||
|
("include_orders", "orders_get_as_df"),
|
||||||
|
("include_terminal", "terminal_info_as_df"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_update_observability_skips_when_disabled(
|
||||||
self,
|
self,
|
||||||
mock_client: MagicMock,
|
mock_client: MagicMock,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
kwarg: str,
|
||||||
|
method: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""include_account=False does not call account_info_as_df."""
|
"""include_X=False does not call the corresponding client method."""
|
||||||
update_observability(
|
update_observability(
|
||||||
client=mock_client,
|
client=mock_client,
|
||||||
output=tmp_path / "obs.db",
|
output=tmp_path / "obs.db",
|
||||||
include_account=False,
|
**{kwarg: False}, # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
mock_client.account_info_as_df.assert_not_called()
|
getattr(mock_client, method).assert_not_called()
|
||||||
|
|
||||||
def test_update_observability_skips_positions_when_disabled(
|
|
||||||
self,
|
|
||||||
mock_client: MagicMock,
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
"""include_positions=False does not call positions_get_as_df."""
|
|
||||||
update_observability(
|
|
||||||
client=mock_client,
|
|
||||||
output=tmp_path / "obs.db",
|
|
||||||
include_positions=False,
|
|
||||||
)
|
|
||||||
mock_client.positions_get_as_df.assert_not_called()
|
|
||||||
|
|
||||||
def test_update_observability_skips_orders_when_disabled(
|
|
||||||
self,
|
|
||||||
mock_client: MagicMock,
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
"""include_orders=False does not call orders_get_as_df."""
|
|
||||||
update_observability(
|
|
||||||
client=mock_client,
|
|
||||||
output=tmp_path / "obs.db",
|
|
||||||
include_orders=False,
|
|
||||||
)
|
|
||||||
mock_client.orders_get_as_df.assert_not_called()
|
|
||||||
|
|
||||||
def test_update_observability_skips_terminal_when_disabled(
|
|
||||||
self,
|
|
||||||
mock_client: MagicMock,
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
"""include_terminal=False does not call terminal_info_as_df."""
|
|
||||||
update_observability(
|
|
||||||
client=mock_client,
|
|
||||||
output=tmp_path / "obs.db",
|
|
||||||
include_terminal=False,
|
|
||||||
)
|
|
||||||
mock_client.terminal_info_as_df.assert_not_called()
|
|
||||||
|
|
||||||
def test_update_observability_with_positions_rows(
|
def test_update_observability_with_positions_rows(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any, cast, get_args
|
from typing import Any, cast, get_args
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
@@ -41,6 +42,7 @@ from mt5cli.trading import (
|
|||||||
extract_tick_price,
|
extract_tick_price,
|
||||||
fetch_latest_closed_rates_for_trading_client,
|
fetch_latest_closed_rates_for_trading_client,
|
||||||
fetch_latest_closed_rates_indexed,
|
fetch_latest_closed_rates_indexed,
|
||||||
|
fetch_recent_history_deals_for_trading_client,
|
||||||
get_account_snapshot,
|
get_account_snapshot,
|
||||||
get_positions_frame,
|
get_positions_frame,
|
||||||
get_symbol_snapshot,
|
get_symbol_snapshot,
|
||||||
@@ -3817,3 +3819,177 @@ class TestCalculatePositionsMarginSafe:
|
|||||||
client = _mock_trade_client()
|
client = _mock_trade_client()
|
||||||
total = calculate_positions_margin_safe(client, symbols=[])
|
total = calculate_positions_margin_safe(client, symbols=[])
|
||||||
_assert_close(total, 0.0)
|
_assert_close(total, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFetchRecentHistoryDealsForTradingClient:
|
||||||
|
"""Tests for fetch_recent_history_deals_for_trading_client."""
|
||||||
|
|
||||||
|
def _fake_client(self, return_value: pd.DataFrame | None) -> MagicMock:
|
||||||
|
client = MagicMock()
|
||||||
|
client.history_deals_get_as_df.return_value = return_value
|
||||||
|
return client
|
||||||
|
|
||||||
|
def test_passes_correct_date_range_and_filters(self) -> None:
|
||||||
|
"""Calls history_deals_get_as_df with derived date_from/date_to."""
|
||||||
|
anchor = datetime(2024, 6, 1, 12, 0, 0, tzinfo=UTC)
|
||||||
|
client = self._fake_client(pd.DataFrame())
|
||||||
|
|
||||||
|
fetch_recent_history_deals_for_trading_client(
|
||||||
|
client,
|
||||||
|
symbol="JP225",
|
||||||
|
group="FX*",
|
||||||
|
hours=6.0,
|
||||||
|
date_to=anchor,
|
||||||
|
)
|
||||||
|
|
||||||
|
client.history_deals_get_as_df.assert_called_once_with(
|
||||||
|
date_from=anchor - timedelta(hours=6.0),
|
||||||
|
date_to=anchor,
|
||||||
|
group="FX*",
|
||||||
|
symbol="JP225",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_raises_for_zero_hours(self) -> None:
|
||||||
|
"""hours=0 raises ValueError."""
|
||||||
|
client = self._fake_client(pd.DataFrame())
|
||||||
|
with pytest.raises(ValueError, match="hours must be finite and positive"):
|
||||||
|
fetch_recent_history_deals_for_trading_client(client, hours=0)
|
||||||
|
|
||||||
|
def test_raises_for_negative_hours(self) -> None:
|
||||||
|
"""Negative hours raises ValueError."""
|
||||||
|
client = self._fake_client(pd.DataFrame())
|
||||||
|
with pytest.raises(ValueError, match="hours must be finite and positive"):
|
||||||
|
fetch_recent_history_deals_for_trading_client(client, hours=-1.0)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad_hours", [float("nan"), float("inf"), float("-inf")])
|
||||||
|
def test_raises_for_non_finite_hours(self, bad_hours: float) -> None:
|
||||||
|
"""nan, inf, and -inf raise ValueError before reaching timedelta."""
|
||||||
|
client = self._fake_client(pd.DataFrame())
|
||||||
|
with pytest.raises(ValueError, match="hours must be finite and positive"):
|
||||||
|
fetch_recent_history_deals_for_trading_client(client, hours=bad_hours)
|
||||||
|
|
||||||
|
def test_none_result_returns_empty_dataframe(self) -> None:
|
||||||
|
"""None from underlying client becomes an empty DataFrame."""
|
||||||
|
client = self._fake_client(None)
|
||||||
|
result = fetch_recent_history_deals_for_trading_client(client, hours=24.0)
|
||||||
|
assert isinstance(result, pd.DataFrame)
|
||||||
|
assert result.empty
|
||||||
|
|
||||||
|
def test_empty_dataframe_result_preserves_schema(self) -> None:
|
||||||
|
"""Empty DataFrame from client is returned with its columns intact."""
|
||||||
|
schema_df = pd.DataFrame(columns=["time", "symbol", "profit", "volume"])
|
||||||
|
client = self._fake_client(schema_df)
|
||||||
|
result = fetch_recent_history_deals_for_trading_client(client, hours=24.0)
|
||||||
|
assert result.empty
|
||||||
|
assert list(result.columns) == ["time", "symbol", "profit", "volume"]
|
||||||
|
|
||||||
|
def test_none_result_returns_bare_empty_dataframe(self) -> None:
|
||||||
|
"""None from client becomes a bare empty DataFrame (no columns)."""
|
||||||
|
client = self._fake_client(None)
|
||||||
|
result = fetch_recent_history_deals_for_trading_client(client, hours=24.0)
|
||||||
|
assert result.empty
|
||||||
|
assert list(result.columns) == []
|
||||||
|
|
||||||
|
def test_sorts_by_time_and_resets_index(self) -> None:
|
||||||
|
"""Unsorted time rows are sorted chronologically and index is reset."""
|
||||||
|
t1 = datetime(2024, 6, 1, 9, 0, tzinfo=UTC)
|
||||||
|
t2 = datetime(2024, 6, 1, 10, 0, tzinfo=UTC)
|
||||||
|
t3 = datetime(2024, 6, 1, 11, 0, tzinfo=UTC)
|
||||||
|
df = pd.DataFrame({"time": [t3, t1, t2], "profit": [3.0, 1.0, 2.0]})
|
||||||
|
client = self._fake_client(df)
|
||||||
|
|
||||||
|
result = fetch_recent_history_deals_for_trading_client(client, hours=24.0)
|
||||||
|
|
||||||
|
assert list(result["time"]) == [t1, t2, t3]
|
||||||
|
assert list(result.index) == [0, 1, 2]
|
||||||
|
|
||||||
|
def test_preserves_all_columns(self) -> None:
|
||||||
|
"""No columns are dropped from the underlying client result."""
|
||||||
|
anchor = datetime(2024, 6, 1, 12, 0, tzinfo=UTC)
|
||||||
|
df = pd.DataFrame({
|
||||||
|
"time": [anchor],
|
||||||
|
"symbol": ["JP225"],
|
||||||
|
"type": [0],
|
||||||
|
"entry": [1],
|
||||||
|
"volume": [0.1],
|
||||||
|
"profit": [50.0],
|
||||||
|
"position_id": [123456],
|
||||||
|
"commission": [-0.5],
|
||||||
|
})
|
||||||
|
client = self._fake_client(df)
|
||||||
|
|
||||||
|
result = fetch_recent_history_deals_for_trading_client(client, hours=24.0)
|
||||||
|
|
||||||
|
assert set(result.columns) == {
|
||||||
|
"time",
|
||||||
|
"symbol",
|
||||||
|
"type",
|
||||||
|
"entry",
|
||||||
|
"volume",
|
||||||
|
"profit",
|
||||||
|
"position_id",
|
||||||
|
"commission",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_no_time_column_still_returns_data(self) -> None:
|
||||||
|
"""DataFrames without a time column are returned with RangeIndex."""
|
||||||
|
df = pd.DataFrame({"profit": [1.0, 2.0], "ticket": [10, 11]})
|
||||||
|
client = self._fake_client(df)
|
||||||
|
|
||||||
|
result = fetch_recent_history_deals_for_trading_client(client, hours=24.0)
|
||||||
|
|
||||||
|
assert list(result["profit"]) == [1.0, 2.0]
|
||||||
|
assert list(result.index) == [0, 1]
|
||||||
|
|
||||||
|
def test_defaults_date_to_to_utc_now(self, mocker: MockerFixture) -> None:
|
||||||
|
"""When date_to is omitted, the window end is datetime.now(UTC)."""
|
||||||
|
frozen = datetime(2024, 6, 1, 0, 0, 0, tzinfo=UTC)
|
||||||
|
mock_dt = mocker.patch("mt5cli.trading.datetime")
|
||||||
|
mock_dt.now.return_value = frozen
|
||||||
|
client = self._fake_client(pd.DataFrame())
|
||||||
|
|
||||||
|
fetch_recent_history_deals_for_trading_client(client, hours=1.0)
|
||||||
|
|
||||||
|
mock_dt.now.assert_called_once_with(UTC)
|
||||||
|
client.history_deals_get_as_df.assert_called_once_with(
|
||||||
|
date_from=frozen - timedelta(hours=1.0),
|
||||||
|
date_to=frozen,
|
||||||
|
group=None,
|
||||||
|
symbol=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateTradingClientHistoryDealsIntegration:
|
||||||
|
"""Verify create_trading_client() result satisfies fetch_recent_history_deals."""
|
||||||
|
|
||||||
|
def test_create_trading_client_result_usable_with_history_deals_helper(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""create_trading_client() result passes directly to fetch_recent_history_deals.
|
||||||
|
|
||||||
|
This exercises the intended SDK call path without a live MT5 terminal.
|
||||||
|
The mock satisfies both _Mt5ClientProtocol and _HistoryDealsClientProtocol.
|
||||||
|
"""
|
||||||
|
mock_raw_client = MagicMock()
|
||||||
|
mocker.patch("mt5cli.trading.Mt5DataClient", return_value=mock_raw_client)
|
||||||
|
|
||||||
|
anchor = datetime(2024, 6, 1, 12, 0, 0, tzinfo=UTC)
|
||||||
|
expected_df = pd.DataFrame({"time": [anchor], "profit": [10.0]})
|
||||||
|
mock_raw_client.history_deals_get_as_df.return_value = expected_df
|
||||||
|
|
||||||
|
client = create_trading_client(login=12345, server="Demo")
|
||||||
|
result = fetch_recent_history_deals_for_trading_client(
|
||||||
|
client,
|
||||||
|
symbol="EURUSD",
|
||||||
|
hours=6.0,
|
||||||
|
date_to=anchor,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_raw_client.history_deals_get_as_df.assert_called_once_with(
|
||||||
|
date_from=anchor - timedelta(hours=6.0),
|
||||||
|
date_to=anchor,
|
||||||
|
group=None,
|
||||||
|
symbol="EURUSD",
|
||||||
|
)
|
||||||
|
assert list(result["profit"]) == [10.0]
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ name = "metatrader5"
|
|||||||
version = "5.0.5640"
|
version = "5.0.5640"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy", marker = "sys_platform == 'win32'" },
|
{ name = "numpy" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/a0/3b764c6743ef601ff12f7d8d62ca5768eb25e90d3758ac7e1d08e667af85/metatrader5-5.0.5640-cp311-cp311-win_amd64.whl", hash = "sha256:4057255f2d63138a3ea1a5d492715038a71d3177cad793fca97a5c58771b4eb1", size = 48091, upload-time = "2026-02-20T23:31:12.289Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/a0/3b764c6743ef601ff12f7d8d62ca5768eb25e90d3758ac7e1d08e667af85/metatrader5-5.0.5640-cp311-cp311-win_amd64.whl", hash = "sha256:4057255f2d63138a3ea1a5d492715038a71d3177cad793fca97a5c58771b4eb1", size = 48091, upload-time = "2026-02-20T23:31:12.289Z" },
|
||||||
@@ -499,7 +499,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mt5cli"
|
name = "mt5cli"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|||||||
Reference in New Issue
Block a user