mt5cli 1.2.2__tar.gz → 1.2.3__tar.gz
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.
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.github/workflows/ci.yml +15 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/AGENTS.md +1 -1
- {mt5cli-1.2.2 → mt5cli-1.2.3}/PKG-INFO +3 -3
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/trading.md +14 -9
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/cli.py +66 -37
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/client.py +25 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/trading.py +53 -10
- {mt5cli-1.2.2 → mt5cli-1.2.3}/pyproject.toml +3 -3
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_cli.py +24 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_trading.py +100 -4
- {mt5cli-1.2.2 → mt5cli-1.2.3}/uv.lock +10 -10
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.claude/agents/codex.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.claude/settings.json +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.github/FUNDING.yml +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.github/dependabot.yml +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.github/renovate.json +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.github/workflows/claude.yml +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.github/workflows/release.yml +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/.gitignore +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/CLAUDE.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/LICENSE +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/README.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/cli.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/client.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/converters.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/exceptions.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/grafana.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/history.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/index.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/marketdata.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/observability.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/public-contract.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/schemas.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/telemetry.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/api/utils.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/docs/index.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/examples/grafana/README.md +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/examples/grafana/compose.yml +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/examples/grafana/dashboards/mt5cli-market.json +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mkdocs.yml +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/__init__.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/__main__.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/contract.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/converters.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/exceptions.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/grafana.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/history.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/marketdata.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/observability.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/retry.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/schemas.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/telemetry.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/mt5cli/utils.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/__init__.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/conftest.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_client.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_contracts.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_examples.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_grafana.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_history.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_marketdata.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_observability.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_packaging.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_telemetry.py +0 -0
- {mt5cli-1.2.2 → mt5cli-1.2.3}/tests/test_utils.py +0 -0
|
@@ -50,6 +50,21 @@ jobs:
|
|
|
50
50
|
with:
|
|
51
51
|
package-path: .
|
|
52
52
|
runs-on: windows-latest
|
|
53
|
+
minimum-dependency-smoke-test:
|
|
54
|
+
if: >
|
|
55
|
+
github.event_name == 'push'
|
|
56
|
+
|| github.event_name == 'pull_request'
|
|
57
|
+
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
|
|
58
|
+
permissions:
|
|
59
|
+
contents: read
|
|
60
|
+
runs-on: windows-latest
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.2
|
|
63
|
+
with:
|
|
64
|
+
persist-credentials: false
|
|
65
|
+
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
|
66
|
+
- name: Check CLI help with minimum Click and Typer versions
|
|
67
|
+
run: uv run --with click==8.4.0 --with typer==0.26.0 mt5cli --help
|
|
53
68
|
python-docs-deploy:
|
|
54
69
|
if: >
|
|
55
70
|
github.event_name == 'push'
|
|
@@ -26,7 +26,7 @@ Apply KISS, DRY, and YAGNI when changing code. Prefer the simplest implementatio
|
|
|
26
26
|
|
|
27
27
|
## Testing Guidelines
|
|
28
28
|
|
|
29
|
-
Tests use pytest, pytest-mock, doctests, and pytest-cov. Test files should match `tests/test_*.py`, classes `Test*`, and functions `test_*`. Coverage is configured with `fail_under = 100`, so add focused tests for every behavior change. Mock MT5/pdmt5 boundaries; do not require a live MetaTrader terminal in unit tests.
|
|
29
|
+
Tests use pytest, pytest-mock, doctests, and pytest-cov. Test files should match `tests/test_*.py`, classes `Test*`, and functions `test_*`. Parametrize unit tests with `pytest.mark.parametrize` when the same behavior needs coverage across multiple inputs. Coverage is configured with `fail_under = 100`, so add focused tests for every behavior change. Mock MT5/pdmt5 boundaries; do not require a live MetaTrader terminal in unit tests.
|
|
30
30
|
|
|
31
31
|
## Commit & Pull Request Guidelines
|
|
32
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Generic MT5 data and execution infrastructure for Python applications
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/mt5cli.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -16,10 +16,10 @@ Classifier: Programming Language :: Python
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
18
|
Requires-Python: <3.14,>=3.11
|
|
19
|
-
Requires-Dist: click>=8.
|
|
19
|
+
Requires-Dist: click>=8.4
|
|
20
20
|
Requires-Dist: pandas>=2.2.2
|
|
21
21
|
Requires-Dist: pdmt5>=1.2.0
|
|
22
|
-
Requires-Dist: typer>=0.
|
|
22
|
+
Requires-Dist: typer>=0.26.0
|
|
23
23
|
Provides-Extra: otel
|
|
24
24
|
Requires-Dist: opentelemetry-api; extra == 'otel'
|
|
25
25
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http; extra == 'otel'
|
|
@@ -202,19 +202,24 @@ or `None` result from the underlying client is normalized to an empty DataFrame.
|
|
|
202
202
|
Downstream packages own all strategy-specific transformations. mt5cli does not
|
|
203
203
|
provide entry-deal classification, Kelly sizing, or any betting-specific helpers.
|
|
204
204
|
|
|
205
|
-
##
|
|
205
|
+
## Timestamp sources and broker clock offsets
|
|
206
206
|
|
|
207
|
-
MT5
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
The MT5 Python documentation describes tick and bar data copied by the
|
|
208
|
+
`copy_ticks_*` and `copy_rates_*` functions as UTC. It does not document a
|
|
209
|
+
different timezone contract for `symbol_info_tick()`, nor should deal epochs
|
|
210
|
+
be assumed to use a broker wall clock without source-specific evidence.
|
|
211
|
+
|
|
212
|
+
`get_tick_snapshot()` preserves the numeric MT5 epoch value in `time`; it does
|
|
213
|
+
not expose pdmt5's timezone-naive `Timestamp` conversion or alter the instant.
|
|
214
|
+
Any broker-specific offset handling therefore requires evidence from the live
|
|
215
|
+
data source in use and must be applied separately. Do not subtract a presumed
|
|
216
|
+
UTC+2 or UTC+3 offset from timestamps that are already UTC.
|
|
212
217
|
|
|
213
218
|
`estimate_server_clock_offset_seconds()` reads the latest tick for a symbol
|
|
214
219
|
and returns the broker's clock offset from true UTC, rounded to the nearest
|
|
215
|
-
half hour, or `None` when no valid tick time is available.
|
|
216
|
-
|
|
217
|
-
|
|
220
|
+
half hour, or `None` when no valid tick time is available. Use it to validate a
|
|
221
|
+
suspected offset for the live `symbol_info_tick()` source before applying the
|
|
222
|
+
result in downstream time-window calculations.
|
|
218
223
|
|
|
219
224
|
No offset is applied automatically by history retrieval helpers.
|
|
220
225
|
|
|
@@ -51,7 +51,7 @@ logger = logging.getLogger(__name__)
|
|
|
51
51
|
# ---------------------------------------------------------------------------
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
class _DateTimeType(click.ParamType):
|
|
54
|
+
class _DateTimeType(click.ParamType[datetime]):
|
|
55
55
|
"""Click parameter type for ISO 8601 datetime strings."""
|
|
56
56
|
|
|
57
57
|
name = "DATETIME"
|
|
@@ -59,8 +59,8 @@ class _DateTimeType(click.ParamType):
|
|
|
59
59
|
def convert(
|
|
60
60
|
self,
|
|
61
61
|
value: object,
|
|
62
|
-
param: click.Parameter | None,
|
|
63
|
-
ctx: click.Context | None,
|
|
62
|
+
param: click.Parameter | None = None,
|
|
63
|
+
ctx: click.Context | None = None,
|
|
64
64
|
) -> datetime:
|
|
65
65
|
"""Convert a string value to a timezone-aware datetime.
|
|
66
66
|
|
|
@@ -80,7 +80,7 @@ class _DateTimeType(click.ParamType):
|
|
|
80
80
|
self.fail(str(exc), param, ctx)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
class _TimeframeType(click.ParamType):
|
|
83
|
+
class _TimeframeType(click.ParamType[int]):
|
|
84
84
|
"""Click parameter type for MT5 timeframe values."""
|
|
85
85
|
|
|
86
86
|
name = "TIMEFRAME"
|
|
@@ -88,8 +88,8 @@ class _TimeframeType(click.ParamType):
|
|
|
88
88
|
def convert(
|
|
89
89
|
self,
|
|
90
90
|
value: object,
|
|
91
|
-
param: click.Parameter | None,
|
|
92
|
-
ctx: click.Context | None,
|
|
91
|
+
param: click.Parameter | None = None,
|
|
92
|
+
ctx: click.Context | None = None,
|
|
93
93
|
) -> int:
|
|
94
94
|
"""Convert a string or integer value to a timeframe integer.
|
|
95
95
|
|
|
@@ -107,7 +107,7 @@ class _TimeframeType(click.ParamType):
|
|
|
107
107
|
self.fail(str(exc), param, ctx)
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
class _TickFlagsType(click.ParamType):
|
|
110
|
+
class _TickFlagsType(click.ParamType[int]):
|
|
111
111
|
"""Click parameter type for MT5 tick copy flags."""
|
|
112
112
|
|
|
113
113
|
name = "FLAGS"
|
|
@@ -115,8 +115,8 @@ class _TickFlagsType(click.ParamType):
|
|
|
115
115
|
def convert(
|
|
116
116
|
self,
|
|
117
117
|
value: object,
|
|
118
|
-
param: click.Parameter | None,
|
|
119
|
-
ctx: click.Context | None,
|
|
118
|
+
param: click.Parameter | None = None,
|
|
119
|
+
ctx: click.Context | None = None,
|
|
120
120
|
) -> int:
|
|
121
121
|
"""Convert a string or integer value to a tick flags integer.
|
|
122
122
|
|
|
@@ -134,7 +134,7 @@ class _TickFlagsType(click.ParamType):
|
|
|
134
134
|
self.fail(str(exc), param, ctx)
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
class _RequestType(click.ParamType):
|
|
137
|
+
class _RequestType(click.ParamType[dict[str, Any]]):
|
|
138
138
|
"""Click parameter type for JSON order requests."""
|
|
139
139
|
|
|
140
140
|
name = "REQUEST"
|
|
@@ -142,8 +142,8 @@ class _RequestType(click.ParamType):
|
|
|
142
142
|
def convert(
|
|
143
143
|
self,
|
|
144
144
|
value: object,
|
|
145
|
-
param: click.Parameter | None,
|
|
146
|
-
ctx: click.Context | None,
|
|
145
|
+
param: click.Parameter | None = None,
|
|
146
|
+
ctx: click.Context | None = None,
|
|
147
147
|
) -> dict[str, Any]:
|
|
148
148
|
"""Convert a raw CLI value to an order request dictionary.
|
|
149
149
|
|
|
@@ -166,6 +166,35 @@ TIMEFRAME_TYPE = _TimeframeType()
|
|
|
166
166
|
TICK_FLAGS_TYPE = _TickFlagsType()
|
|
167
167
|
REQUEST_TYPE = _RequestType()
|
|
168
168
|
|
|
169
|
+
|
|
170
|
+
def _parse_datetime_parameter(value: str) -> datetime:
|
|
171
|
+
try:
|
|
172
|
+
return parse_datetime(value)
|
|
173
|
+
except ValueError as exc:
|
|
174
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _parse_timeframe_parameter(value: str) -> int:
|
|
178
|
+
try:
|
|
179
|
+
return parse_timeframe(value)
|
|
180
|
+
except ValueError as exc:
|
|
181
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _parse_tick_flags_parameter(value: str) -> int:
|
|
185
|
+
try:
|
|
186
|
+
return parse_tick_flags(value)
|
|
187
|
+
except ValueError as exc:
|
|
188
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _parse_request_parameter(value: str) -> dict[str, Any]:
|
|
192
|
+
try:
|
|
193
|
+
return parse_request(value)
|
|
194
|
+
except ValueError as exc:
|
|
195
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
196
|
+
|
|
197
|
+
|
|
169
198
|
# ---------------------------------------------------------------------------
|
|
170
199
|
# Export context
|
|
171
200
|
# ---------------------------------------------------------------------------
|
|
@@ -363,14 +392,14 @@ def rates_from(
|
|
|
363
392
|
timeframe: Annotated[
|
|
364
393
|
int,
|
|
365
394
|
typer.Option(
|
|
366
|
-
|
|
395
|
+
parser=_parse_timeframe_parameter,
|
|
367
396
|
help="Timeframe (e.g., M1, H1, D1, or integer).",
|
|
368
397
|
),
|
|
369
398
|
],
|
|
370
399
|
date_from: Annotated[
|
|
371
400
|
datetime,
|
|
372
401
|
typer.Option(
|
|
373
|
-
|
|
402
|
+
parser=_parse_datetime_parameter,
|
|
374
403
|
help="Start date in ISO 8601 format.",
|
|
375
404
|
),
|
|
376
405
|
],
|
|
@@ -390,7 +419,7 @@ def rates_from_pos(
|
|
|
390
419
|
timeframe: Annotated[
|
|
391
420
|
int,
|
|
392
421
|
typer.Option(
|
|
393
|
-
|
|
422
|
+
parser=_parse_timeframe_parameter,
|
|
394
423
|
help="Timeframe.",
|
|
395
424
|
),
|
|
396
425
|
],
|
|
@@ -416,7 +445,7 @@ def latest_rates(
|
|
|
416
445
|
timeframe: Annotated[
|
|
417
446
|
int,
|
|
418
447
|
typer.Option(
|
|
419
|
-
|
|
448
|
+
parser=_parse_timeframe_parameter,
|
|
420
449
|
help="Timeframe.",
|
|
421
450
|
),
|
|
422
451
|
],
|
|
@@ -445,17 +474,17 @@ def rates_range(
|
|
|
445
474
|
timeframe: Annotated[
|
|
446
475
|
int,
|
|
447
476
|
typer.Option(
|
|
448
|
-
|
|
477
|
+
parser=_parse_timeframe_parameter,
|
|
449
478
|
help="Timeframe.",
|
|
450
479
|
),
|
|
451
480
|
],
|
|
452
481
|
date_from: Annotated[
|
|
453
482
|
datetime,
|
|
454
|
-
typer.Option(
|
|
483
|
+
typer.Option(parser=_parse_datetime_parameter, help="Start date."),
|
|
455
484
|
],
|
|
456
485
|
date_to: Annotated[
|
|
457
486
|
datetime,
|
|
458
|
-
typer.Option(
|
|
487
|
+
typer.Option(parser=_parse_datetime_parameter, help="End date."),
|
|
459
488
|
],
|
|
460
489
|
) -> None:
|
|
461
490
|
"""Export rates for a date range."""
|
|
@@ -471,13 +500,13 @@ def ticks_from(
|
|
|
471
500
|
symbol: Annotated[str, typer.Option(help="Symbol name.")],
|
|
472
501
|
date_from: Annotated[
|
|
473
502
|
datetime,
|
|
474
|
-
typer.Option(
|
|
503
|
+
typer.Option(parser=_parse_datetime_parameter, help="Start date."),
|
|
475
504
|
],
|
|
476
505
|
count: Annotated[int, typer.Option(help="Number of ticks.")],
|
|
477
506
|
flags: Annotated[
|
|
478
507
|
int,
|
|
479
508
|
typer.Option(
|
|
480
|
-
|
|
509
|
+
parser=_parse_tick_flags_parameter,
|
|
481
510
|
help="Tick flags (ALL, INFO, TRADE, or integer).",
|
|
482
511
|
),
|
|
483
512
|
],
|
|
@@ -495,15 +524,15 @@ def ticks_range(
|
|
|
495
524
|
symbol: Annotated[str, typer.Option(help="Symbol name.")],
|
|
496
525
|
date_from: Annotated[
|
|
497
526
|
datetime,
|
|
498
|
-
typer.Option(
|
|
527
|
+
typer.Option(parser=_parse_datetime_parameter, help="Start date."),
|
|
499
528
|
],
|
|
500
529
|
date_to: Annotated[
|
|
501
530
|
datetime,
|
|
502
|
-
typer.Option(
|
|
531
|
+
typer.Option(parser=_parse_datetime_parameter, help="End date."),
|
|
503
532
|
],
|
|
504
533
|
flags: Annotated[
|
|
505
534
|
int,
|
|
506
|
-
typer.Option(
|
|
535
|
+
typer.Option(parser=_parse_tick_flags_parameter, help="Tick flags."),
|
|
507
536
|
],
|
|
508
537
|
) -> None:
|
|
509
538
|
"""Export ticks for a date range."""
|
|
@@ -523,7 +552,7 @@ def ticks_recent(
|
|
|
523
552
|
],
|
|
524
553
|
date_to: Annotated[
|
|
525
554
|
datetime | None,
|
|
526
|
-
typer.Option(
|
|
555
|
+
typer.Option(parser=_parse_datetime_parameter, help="Window end date."),
|
|
527
556
|
] = None,
|
|
528
557
|
count: Annotated[
|
|
529
558
|
int,
|
|
@@ -532,7 +561,7 @@ def ticks_recent(
|
|
|
532
561
|
flags: Annotated[
|
|
533
562
|
int,
|
|
534
563
|
typer.Option(
|
|
535
|
-
|
|
564
|
+
parser=_parse_tick_flags_parameter,
|
|
536
565
|
help="Tick flags (ALL, INFO, TRADE, or integer).",
|
|
537
566
|
),
|
|
538
567
|
] = "ALL", # pyright: ignore[reportArgumentType]
|
|
@@ -625,11 +654,11 @@ def history_orders(
|
|
|
625
654
|
ctx: typer.Context,
|
|
626
655
|
date_from: Annotated[
|
|
627
656
|
datetime | None,
|
|
628
|
-
typer.Option(
|
|
657
|
+
typer.Option(parser=_parse_datetime_parameter, help="Start date."),
|
|
629
658
|
] = None,
|
|
630
659
|
date_to: Annotated[
|
|
631
660
|
datetime | None,
|
|
632
|
-
typer.Option(
|
|
661
|
+
typer.Option(parser=_parse_datetime_parameter, help="End date."),
|
|
633
662
|
] = None,
|
|
634
663
|
group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
|
|
635
664
|
symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
|
|
@@ -655,11 +684,11 @@ def history_deals(
|
|
|
655
684
|
ctx: typer.Context,
|
|
656
685
|
date_from: Annotated[
|
|
657
686
|
datetime | None,
|
|
658
|
-
typer.Option(
|
|
687
|
+
typer.Option(parser=_parse_datetime_parameter, help="Start date."),
|
|
659
688
|
] = None,
|
|
660
689
|
date_to: Annotated[
|
|
661
690
|
datetime | None,
|
|
662
|
-
typer.Option(
|
|
691
|
+
typer.Option(parser=_parse_datetime_parameter, help="End date."),
|
|
663
692
|
] = None,
|
|
664
693
|
group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
|
|
665
694
|
symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
|
|
@@ -686,7 +715,7 @@ def recent_history_deals(
|
|
|
686
715
|
hours: Annotated[float, typer.Option(help="Lookback window in hours.")],
|
|
687
716
|
date_to: Annotated[
|
|
688
717
|
datetime | None,
|
|
689
|
-
typer.Option(
|
|
718
|
+
typer.Option(parser=_parse_datetime_parameter, help="Window end date."),
|
|
690
719
|
] = None,
|
|
691
720
|
group: Annotated[str | None, typer.Option(help="Group filter.")] = None,
|
|
692
721
|
symbol: Annotated[str | None, typer.Option(help="Symbol filter.")] = None,
|
|
@@ -744,7 +773,7 @@ def order_check(
|
|
|
744
773
|
ctx: typer.Context,
|
|
745
774
|
request: Annotated[
|
|
746
775
|
dict[str, Any],
|
|
747
|
-
typer.Option(
|
|
776
|
+
typer.Option(parser=_parse_request_parameter, help=_REQUEST_OPTION_HELP),
|
|
748
777
|
],
|
|
749
778
|
) -> None:
|
|
750
779
|
"""Check funds sufficiency for a trading operation."""
|
|
@@ -756,7 +785,7 @@ def order_send(
|
|
|
756
785
|
ctx: typer.Context,
|
|
757
786
|
request: Annotated[
|
|
758
787
|
dict[str, Any],
|
|
759
|
-
typer.Option(
|
|
788
|
+
typer.Option(parser=_parse_request_parameter, help=_REQUEST_OPTION_HELP),
|
|
760
789
|
],
|
|
761
790
|
yes: Annotated[
|
|
762
791
|
bool,
|
|
@@ -995,11 +1024,11 @@ def collect_history(
|
|
|
995
1024
|
],
|
|
996
1025
|
date_from: Annotated[
|
|
997
1026
|
datetime,
|
|
998
|
-
typer.Option(
|
|
1027
|
+
typer.Option(parser=_parse_datetime_parameter, help="Start date."),
|
|
999
1028
|
],
|
|
1000
1029
|
date_to: Annotated[
|
|
1001
1030
|
datetime,
|
|
1002
|
-
typer.Option(
|
|
1031
|
+
typer.Option(parser=_parse_datetime_parameter, help="End date."),
|
|
1003
1032
|
],
|
|
1004
1033
|
dataset: Annotated[
|
|
1005
1034
|
list[Dataset] | None,
|
|
@@ -1016,14 +1045,14 @@ def collect_history(
|
|
|
1016
1045
|
timeframe: Annotated[
|
|
1017
1046
|
int,
|
|
1018
1047
|
typer.Option(
|
|
1019
|
-
|
|
1048
|
+
parser=_parse_timeframe_parameter,
|
|
1020
1049
|
help="Rates timeframe (e.g., M1, H1, D1).",
|
|
1021
1050
|
),
|
|
1022
1051
|
] = 1,
|
|
1023
1052
|
flags: Annotated[
|
|
1024
1053
|
int,
|
|
1025
1054
|
typer.Option(
|
|
1026
|
-
|
|
1055
|
+
parser=_parse_tick_flags_parameter,
|
|
1027
1056
|
help="Tick copy flags (ALL, INFO, TRADE, or integer).",
|
|
1028
1057
|
),
|
|
1029
1058
|
] = "ALL", # pyright: ignore[reportArgumentType]
|
|
@@ -787,6 +787,31 @@ class _BaseMT5Client:
|
|
|
787
787
|
"""Return the last tick for a symbol."""
|
|
788
788
|
return self._fetch(lambda c: c.symbol_info_tick_as_df(symbol=symbol))
|
|
789
789
|
|
|
790
|
+
def symbol_info_tick_as_dict(
|
|
791
|
+
self,
|
|
792
|
+
symbol: str,
|
|
793
|
+
*,
|
|
794
|
+
skip_to_datetime: bool = False,
|
|
795
|
+
) -> dict[str, object]:
|
|
796
|
+
"""Return the last tick as a plain mapping.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
symbol: Symbol name.
|
|
800
|
+
skip_to_datetime: Preserve numeric MT5 time fields when ``True``.
|
|
801
|
+
|
|
802
|
+
Returns:
|
|
803
|
+
Tick fields using the underlying pdmt5 conversion behavior.
|
|
804
|
+
"""
|
|
805
|
+
return self._fetch_value(
|
|
806
|
+
lambda c: cast(
|
|
807
|
+
"dict[str, object]",
|
|
808
|
+
c.symbol_info_tick_as_dict(
|
|
809
|
+
symbol=symbol,
|
|
810
|
+
skip_to_datetime=skip_to_datetime,
|
|
811
|
+
),
|
|
812
|
+
)
|
|
813
|
+
)
|
|
814
|
+
|
|
790
815
|
def market_book(self, symbol: str) -> pd.DataFrame:
|
|
791
816
|
"""Return market depth for a symbol."""
|
|
792
817
|
return self._fetch(lambda c: c.market_book_get_as_df(symbol=symbol))
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
from dataclasses import asdict, dataclass
|
|
7
7
|
from datetime import UTC, datetime
|
|
8
|
+
from inspect import signature
|
|
8
9
|
from math import floor, isfinite
|
|
9
10
|
from numbers import Integral, Real
|
|
10
11
|
from typing import TYPE_CHECKING, Literal, Protocol, TypedDict, cast
|
|
@@ -20,6 +21,7 @@ if TYPE_CHECKING:
|
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
_logger = logging.getLogger(__name__)
|
|
24
|
+
_DATETIME_TYPES = (datetime, pd.Timestamp)
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class _Mt5ClientProtocol(Protocol):
|
|
@@ -801,16 +803,57 @@ def get_tick_snapshot(
|
|
|
801
803
|
client: _Mt5ClientProtocol,
|
|
802
804
|
symbol: str,
|
|
803
805
|
) -> dict[str, float | int | None]:
|
|
804
|
-
"""Return normalized latest tick data
|
|
806
|
+
"""Return normalized latest tick data with a numeric MT5 timestamp.
|
|
807
|
+
|
|
808
|
+
The numeric ``time`` preserves the original value returned by MT5 without
|
|
809
|
+
applying timezone or server-offset correction.
|
|
810
|
+
"""
|
|
805
811
|
method = getattr(client, "symbol_info_tick_as_dict", None)
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
812
|
+
if callable(method):
|
|
813
|
+
try:
|
|
814
|
+
signature(method).bind(symbol=symbol, skip_to_datetime=True)
|
|
815
|
+
except (TypeError, ValueError):
|
|
816
|
+
value = method(symbol=symbol)
|
|
817
|
+
else:
|
|
818
|
+
value = method(symbol=symbol, skip_to_datetime=True)
|
|
819
|
+
else:
|
|
820
|
+
value = client.symbol_info_tick(symbol)
|
|
809
821
|
snapshot = _snapshot_from_value(value, _TICK_SNAPSHOT_FIELDS)
|
|
810
822
|
snapshot["symbol"] = snapshot.get("symbol") or symbol
|
|
823
|
+
snapshot["time"] = _numeric_tick_time(snapshot.get("time"))
|
|
811
824
|
return cast("dict[str, float | int | None]", snapshot)
|
|
812
825
|
|
|
813
826
|
|
|
827
|
+
def _numeric_tick_time(value: object) -> float | int | None:
|
|
828
|
+
"""Normalize an MT5 timestamp to its numeric epoch representation.
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
The numeric timestamp, or ``None`` for an unsupported value.
|
|
832
|
+
"""
|
|
833
|
+
numeric: float | None
|
|
834
|
+
if value is None or isinstance(value, bool):
|
|
835
|
+
numeric = None
|
|
836
|
+
elif isinstance(value, _DATETIME_TYPES):
|
|
837
|
+
if pd.isna(value):
|
|
838
|
+
return None
|
|
839
|
+
timestamp = pd.Timestamp(value)
|
|
840
|
+
if timestamp.tzinfo is None:
|
|
841
|
+
timestamp = timestamp.tz_localize(UTC)
|
|
842
|
+
numeric = timestamp.timestamp()
|
|
843
|
+
elif isinstance(value, Integral):
|
|
844
|
+
return int(value)
|
|
845
|
+
elif isinstance(value, Real):
|
|
846
|
+
numeric = float(value)
|
|
847
|
+
elif isinstance(value, str):
|
|
848
|
+
try:
|
|
849
|
+
numeric = float(value)
|
|
850
|
+
except ValueError:
|
|
851
|
+
return None
|
|
852
|
+
else:
|
|
853
|
+
numeric = None
|
|
854
|
+
return numeric if numeric is not None and isfinite(numeric) else None
|
|
855
|
+
|
|
856
|
+
|
|
814
857
|
_SERVER_CLOCK_OFFSET_ROUNDING_SECONDS = 1800.0
|
|
815
858
|
_MAX_PLAUSIBLE_SERVER_CLOCK_OFFSET_SECONDS = 14 * 3600.0
|
|
816
859
|
|
|
@@ -821,12 +864,12 @@ def estimate_server_clock_offset_seconds(
|
|
|
821
864
|
) -> float | None:
|
|
822
865
|
"""Estimate the broker server clock offset from true UTC.
|
|
823
866
|
|
|
824
|
-
MT5 tick
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
867
|
+
MT5 documents copied tick and bar data as UTC, while the timezone contract
|
|
868
|
+
for ``symbol_info_tick()`` is not explicit. This helper reads the latest
|
|
869
|
+
tick for ``symbol`` to validate a suspected broker-specific clock offset
|
|
870
|
+
before downstream correction. It compares the tick timestamp to the
|
|
871
|
+
current UTC time and rounds to the nearest half hour, absorbing a few
|
|
872
|
+
minutes of real tick staleness.
|
|
830
873
|
|
|
831
874
|
On a closed market, weekend, holiday, or illiquid symbol the latest tick
|
|
832
875
|
can be hours or days old, in which case the tick-vs-now delta reflects
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5cli"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.3"
|
|
4
4
|
description = "Generic MT5 data and execution infrastructure for Python applications"
|
|
5
5
|
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
6
6
|
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
@@ -11,8 +11,8 @@ requires-python = ">= 3.11, < 3.14"
|
|
|
11
11
|
dependencies = [
|
|
12
12
|
"pdmt5 >= 1.2.0",
|
|
13
13
|
"pandas >= 2.2.2",
|
|
14
|
-
"click >= 8.
|
|
15
|
-
"typer >= 0.
|
|
14
|
+
"click >= 8.4",
|
|
15
|
+
"typer >= 0.26.0",
|
|
16
16
|
]
|
|
17
17
|
classifiers = [
|
|
18
18
|
"Development Status :: 3 - Alpha",
|
|
@@ -25,6 +25,10 @@ from mt5cli.cli import (
|
|
|
25
25
|
TIMEFRAME_TYPE, # type: ignore[reportPrivateUsage]
|
|
26
26
|
_execute_export, # type: ignore[reportPrivateUsage]
|
|
27
27
|
_ExportContext, # type: ignore[reportPrivateUsage]
|
|
28
|
+
_parse_datetime_parameter, # type: ignore[reportPrivateUsage]
|
|
29
|
+
_parse_request_parameter, # type: ignore[reportPrivateUsage]
|
|
30
|
+
_parse_tick_flags_parameter, # type: ignore[reportPrivateUsage]
|
|
31
|
+
_parse_timeframe_parameter, # type: ignore[reportPrivateUsage]
|
|
28
32
|
_sdk_client, # type: ignore[reportPrivateUsage]
|
|
29
33
|
app,
|
|
30
34
|
main,
|
|
@@ -2195,6 +2199,11 @@ class TestDateTimeType:
|
|
|
2195
2199
|
with pytest.raises(Exception, match="Invalid datetime"):
|
|
2196
2200
|
DATETIME_TYPE.convert("bad", None, None)
|
|
2197
2201
|
|
|
2202
|
+
def test_parser_invalid(self) -> None:
|
|
2203
|
+
"""Test that Typer receives a BadParameter with validation details."""
|
|
2204
|
+
with pytest.raises(Exception, match="Invalid datetime"):
|
|
2205
|
+
_parse_datetime_parameter("bad")
|
|
2206
|
+
|
|
2198
2207
|
|
|
2199
2208
|
class TestTimeframeType:
|
|
2200
2209
|
"""Tests for _TimeframeType."""
|
|
@@ -2224,6 +2233,11 @@ class TestTimeframeType:
|
|
|
2224
2233
|
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
2225
2234
|
TIMEFRAME_TYPE.convert(value, None, None)
|
|
2226
2235
|
|
|
2236
|
+
def test_parser_invalid(self) -> None:
|
|
2237
|
+
"""Test that Typer receives a BadParameter with validation details."""
|
|
2238
|
+
with pytest.raises(Exception, match="Invalid timeframe"):
|
|
2239
|
+
_parse_timeframe_parameter("bad")
|
|
2240
|
+
|
|
2227
2241
|
|
|
2228
2242
|
class TestTickFlagsType:
|
|
2229
2243
|
"""Tests for _TickFlagsType."""
|
|
@@ -2253,6 +2267,11 @@ class TestTickFlagsType:
|
|
|
2253
2267
|
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
2254
2268
|
TICK_FLAGS_TYPE.convert(value, None, None)
|
|
2255
2269
|
|
|
2270
|
+
def test_parser_invalid(self) -> None:
|
|
2271
|
+
"""Test that Typer receives a BadParameter with validation details."""
|
|
2272
|
+
with pytest.raises(Exception, match="Invalid tick flags"):
|
|
2273
|
+
_parse_tick_flags_parameter("bad")
|
|
2274
|
+
|
|
2256
2275
|
|
|
2257
2276
|
class TestRequestType:
|
|
2258
2277
|
"""Tests for _RequestType."""
|
|
@@ -2265,3 +2284,8 @@ class TestRequestType:
|
|
|
2265
2284
|
"""Test that invalid values raise BadParameter."""
|
|
2266
2285
|
with pytest.raises(Exception, match="Invalid JSON request"):
|
|
2267
2286
|
REQUEST_TYPE.convert("bad", None, None)
|
|
2287
|
+
|
|
2288
|
+
def test_parser_invalid(self) -> None:
|
|
2289
|
+
"""Test that Typer receives a BadParameter with validation details."""
|
|
2290
|
+
with pytest.raises(Exception, match="Invalid JSON request"):
|
|
2291
|
+
_parse_request_parameter("bad")
|
|
@@ -15,6 +15,7 @@ from pdmt5 import Mt5RuntimeError
|
|
|
15
15
|
from pytest_mock import MockerFixture # noqa: TC002
|
|
16
16
|
|
|
17
17
|
from mt5cli import trading
|
|
18
|
+
from mt5cli.client import MT5Client
|
|
18
19
|
from mt5cli.exceptions import Mt5OperationError
|
|
19
20
|
from mt5cli.trading import (
|
|
20
21
|
MarginVolume,
|
|
@@ -671,6 +672,99 @@ class TestSnapshotsAndState:
|
|
|
671
672
|
assert get_symbol_snapshot(client, "EURUSD")["symbol"] == "EURUSD"
|
|
672
673
|
assert get_tick_snapshot(client, "EURUSD")["symbol"] == "EURUSD"
|
|
673
674
|
|
|
675
|
+
def test_tick_snapshot_requests_raw_time_from_mt5_client(self) -> None:
|
|
676
|
+
"""Test the canonical client preserves pdmt5's numeric MT5 time."""
|
|
677
|
+
|
|
678
|
+
class ConnectedClient:
|
|
679
|
+
def symbol_info_tick_as_dict(
|
|
680
|
+
self,
|
|
681
|
+
symbol: str,
|
|
682
|
+
skip_to_datetime: bool = False,
|
|
683
|
+
) -> dict[str, float]:
|
|
684
|
+
assert symbol == "USOIL"
|
|
685
|
+
assert skip_to_datetime is True
|
|
686
|
+
return {"time": 1717250400, "bid": 78.5, "ask": 78.6}
|
|
687
|
+
|
|
688
|
+
client = MT5Client.from_connected_client(ConnectedClient())
|
|
689
|
+
|
|
690
|
+
snapshot = get_tick_snapshot(client, "USOIL")
|
|
691
|
+
|
|
692
|
+
assert snapshot["time"] == 1717250400
|
|
693
|
+
_assert_close(snapshot["bid"], 78.5)
|
|
694
|
+
_assert_close(snapshot["ask"], 78.6)
|
|
695
|
+
|
|
696
|
+
def test_tick_snapshot_supports_dict_method_without_skip_keyword(self) -> None:
|
|
697
|
+
"""Test compatible dictionary clients need not accept pdmt5 options."""
|
|
698
|
+
|
|
699
|
+
class DictionaryClient:
|
|
700
|
+
def symbol_info_tick_as_dict(self, *, symbol: str) -> dict[str, float]:
|
|
701
|
+
assert symbol == "UKOIL"
|
|
702
|
+
return {"time": 1717254000, "bid": 80.1, "ask": 80.2}
|
|
703
|
+
|
|
704
|
+
snapshot = get_tick_snapshot(
|
|
705
|
+
cast("_Mt5ClientProtocol", DictionaryClient()),
|
|
706
|
+
"UKOIL",
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
assert snapshot["time"] == 1717254000
|
|
710
|
+
|
|
711
|
+
@pytest.mark.parametrize(
|
|
712
|
+
"tick_time",
|
|
713
|
+
[
|
|
714
|
+
pd.Timestamp("2024-06-01 15:00:00"),
|
|
715
|
+
pd.Timestamp("2024-06-01 15:00:00", tz=UTC),
|
|
716
|
+
],
|
|
717
|
+
ids=["naive", "timezone-aware"],
|
|
718
|
+
)
|
|
719
|
+
def test_tick_snapshot_normalizes_dataframe_timestamp_to_numeric(
|
|
720
|
+
self,
|
|
721
|
+
tick_time: pd.Timestamp,
|
|
722
|
+
) -> None:
|
|
723
|
+
"""Test DataFrame fallbacks cannot leak a naive pandas Timestamp."""
|
|
724
|
+
client = MagicMock()
|
|
725
|
+
del client.symbol_info_tick_as_dict
|
|
726
|
+
client.symbol_info_tick.return_value = pd.DataFrame(
|
|
727
|
+
[{"time": tick_time, "bid": 1.0}],
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
snapshot = get_tick_snapshot(client, "EURUSD")
|
|
731
|
+
|
|
732
|
+
_assert_close(snapshot["time"], 1717254000.0)
|
|
733
|
+
assert not isinstance(snapshot["time"], pd.Timestamp)
|
|
734
|
+
|
|
735
|
+
@pytest.mark.parametrize(
|
|
736
|
+
("tick_time", "expected"),
|
|
737
|
+
[
|
|
738
|
+
(True, None),
|
|
739
|
+
(float("inf"), None),
|
|
740
|
+
(pd.NaT, None),
|
|
741
|
+
("1717254000", 1717254000.0),
|
|
742
|
+
(object(), None),
|
|
743
|
+
],
|
|
744
|
+
ids=[
|
|
745
|
+
"boolean",
|
|
746
|
+
"non-finite",
|
|
747
|
+
"missing-datetime",
|
|
748
|
+
"numeric-string",
|
|
749
|
+
"unsupported",
|
|
750
|
+
],
|
|
751
|
+
)
|
|
752
|
+
def test_tick_snapshot_time_always_matches_numeric_contract(
|
|
753
|
+
self,
|
|
754
|
+
tick_time: object,
|
|
755
|
+
expected: float | None,
|
|
756
|
+
) -> None:
|
|
757
|
+
"""Test non-canonical time payloads normalize to a number or None."""
|
|
758
|
+
client = MagicMock()
|
|
759
|
+
client.symbol_info_tick_as_dict.return_value = {"time": tick_time}
|
|
760
|
+
|
|
761
|
+
snapshot = get_tick_snapshot(client, "EURUSD")
|
|
762
|
+
|
|
763
|
+
if expected is None:
|
|
764
|
+
assert snapshot["time"] is None
|
|
765
|
+
else:
|
|
766
|
+
_assert_close(snapshot["time"], expected)
|
|
767
|
+
|
|
674
768
|
def test_symbol_and_tick_snapshots_use_object_fallbacks(self) -> None:
|
|
675
769
|
"""Test symbol and tick snapshots support non-dict MT5 values."""
|
|
676
770
|
client = MagicMock()
|
|
@@ -3983,19 +4077,21 @@ class TestEstimateServerClockOffsetSeconds:
|
|
|
3983
4077
|
client.symbol_info_tick_as_dict.return_value = {"time": tick_time}
|
|
3984
4078
|
return client
|
|
3985
4079
|
|
|
3986
|
-
|
|
4080
|
+
@pytest.mark.parametrize("server_offset", [7200.0, 10800.0])
|
|
4081
|
+
def test_rounds_positive_broker_offset_to_nearest_half_hour(
|
|
3987
4082
|
self,
|
|
3988
4083
|
mocker: MockerFixture,
|
|
4084
|
+
server_offset: float,
|
|
3989
4085
|
) -> None:
|
|
3990
|
-
"""
|
|
4086
|
+
"""UTC+2 and UTC+3 broker tick labels yield their server offsets."""
|
|
3991
4087
|
frozen = datetime(2024, 6, 1, 12, 0, 0, tzinfo=UTC)
|
|
3992
4088
|
mock_dt = mocker.patch("mt5cli.trading.datetime")
|
|
3993
4089
|
mock_dt.now.return_value = frozen
|
|
3994
|
-
client = self._client_with_tick_time(frozen.timestamp() +
|
|
4090
|
+
client = self._client_with_tick_time(frozen.timestamp() + server_offset + 0.4)
|
|
3995
4091
|
|
|
3996
4092
|
offset = estimate_server_clock_offset_seconds(client, "EURUSD")
|
|
3997
4093
|
|
|
3998
|
-
_assert_close(offset,
|
|
4094
|
+
_assert_close(offset, server_offset)
|
|
3999
4095
|
|
|
4000
4096
|
def test_stale_tick_on_utc_server_rounds_to_zero(
|
|
4001
4097
|
self,
|
|
@@ -118,14 +118,14 @@ wheels = [
|
|
|
118
118
|
|
|
119
119
|
[[package]]
|
|
120
120
|
name = "click"
|
|
121
|
-
version = "8.
|
|
121
|
+
version = "8.4.2"
|
|
122
122
|
source = { registry = "https://pypi.org/simple" }
|
|
123
123
|
dependencies = [
|
|
124
124
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
125
125
|
]
|
|
126
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
126
|
+
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
|
|
127
127
|
wheels = [
|
|
128
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
128
|
+
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
|
|
129
129
|
]
|
|
130
130
|
|
|
131
131
|
[[package]]
|
|
@@ -499,7 +499,7 @@ wheels = [
|
|
|
499
499
|
|
|
500
500
|
[[package]]
|
|
501
501
|
name = "mt5cli"
|
|
502
|
-
version = "1.2.
|
|
502
|
+
version = "1.2.3"
|
|
503
503
|
source = { editable = "." }
|
|
504
504
|
dependencies = [
|
|
505
505
|
{ name = "click" },
|
|
@@ -537,14 +537,14 @@ dev = [
|
|
|
537
537
|
|
|
538
538
|
[package.metadata]
|
|
539
539
|
requires-dist = [
|
|
540
|
-
{ name = "click", specifier = ">=8.
|
|
540
|
+
{ name = "click", specifier = ">=8.4" },
|
|
541
541
|
{ name = "opentelemetry-api", marker = "extra == 'otel'" },
|
|
542
542
|
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otel'" },
|
|
543
543
|
{ name = "opentelemetry-sdk", marker = "extra == 'otel'" },
|
|
544
544
|
{ name = "pandas", specifier = ">=2.2.2" },
|
|
545
545
|
{ name = "pdmt5", specifier = ">=1.2.0" },
|
|
546
546
|
{ name = "pyarrow", marker = "extra == 'parquet'", specifier = ">=19.0.0" },
|
|
547
|
-
{ name = "typer", specifier = ">=0.
|
|
547
|
+
{ name = "typer", specifier = ">=0.26.0" },
|
|
548
548
|
]
|
|
549
549
|
provides-extras = ["parquet", "otel"]
|
|
550
550
|
|
|
@@ -1213,17 +1213,17 @@ wheels = [
|
|
|
1213
1213
|
|
|
1214
1214
|
[[package]]
|
|
1215
1215
|
name = "typer"
|
|
1216
|
-
version = "0.
|
|
1216
|
+
version = "0.26.8"
|
|
1217
1217
|
source = { registry = "https://pypi.org/simple" }
|
|
1218
1218
|
dependencies = [
|
|
1219
1219
|
{ name = "annotated-doc" },
|
|
1220
|
-
{ name = "
|
|
1220
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
1221
1221
|
{ name = "rich" },
|
|
1222
1222
|
{ name = "shellingham" },
|
|
1223
1223
|
]
|
|
1224
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1224
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" }
|
|
1225
1225
|
wheels = [
|
|
1226
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1226
|
+
{ url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" },
|
|
1227
1227
|
]
|
|
1228
1228
|
|
|
1229
1229
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|