mt5cli 1.3.0__tar.gz → 1.3.1__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.
Files changed (73) hide show
  1. {mt5cli-1.3.0 → mt5cli-1.3.1}/PKG-INFO +1 -1
  2. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/trading.md +10 -0
  3. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/trading.py +14 -3
  4. {mt5cli-1.3.0 → mt5cli-1.3.1}/pyproject.toml +3 -1
  5. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_trading.py +107 -2
  6. {mt5cli-1.3.0 → mt5cli-1.3.1}/uv.lock +8 -1
  7. {mt5cli-1.3.0 → mt5cli-1.3.1}/.agents/skills/local-qa/SKILL.md +0 -0
  8. {mt5cli-1.3.0 → mt5cli-1.3.1}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  9. {mt5cli-1.3.0 → mt5cli-1.3.1}/.agents/skills/mt5cli/SKILL.md +0 -0
  10. {mt5cli-1.3.0 → mt5cli-1.3.1}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
  11. {mt5cli-1.3.0 → mt5cli-1.3.1}/.claude/agents/codex.md +0 -0
  12. {mt5cli-1.3.0 → mt5cli-1.3.1}/.claude/settings.json +0 -0
  13. {mt5cli-1.3.0 → mt5cli-1.3.1}/.github/FUNDING.yml +0 -0
  14. {mt5cli-1.3.0 → mt5cli-1.3.1}/.github/dependabot.yml +0 -0
  15. {mt5cli-1.3.0 → mt5cli-1.3.1}/.github/renovate.json +0 -0
  16. {mt5cli-1.3.0 → mt5cli-1.3.1}/.github/workflows/ci.yml +0 -0
  17. {mt5cli-1.3.0 → mt5cli-1.3.1}/.github/workflows/claude.yml +0 -0
  18. {mt5cli-1.3.0 → mt5cli-1.3.1}/.github/workflows/release.yml +0 -0
  19. {mt5cli-1.3.0 → mt5cli-1.3.1}/.gitignore +0 -0
  20. {mt5cli-1.3.0 → mt5cli-1.3.1}/AGENTS.md +0 -0
  21. {mt5cli-1.3.0 → mt5cli-1.3.1}/CLAUDE.md +0 -0
  22. {mt5cli-1.3.0 → mt5cli-1.3.1}/LICENSE +0 -0
  23. {mt5cli-1.3.0 → mt5cli-1.3.1}/README.md +0 -0
  24. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/cli.md +0 -0
  25. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/client.md +0 -0
  26. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/converters.md +0 -0
  27. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/exceptions.md +0 -0
  28. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/grafana.md +0 -0
  29. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/history.md +0 -0
  30. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/index.md +0 -0
  31. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/marketdata.md +0 -0
  32. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/observability.md +0 -0
  33. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/public-contract.md +0 -0
  34. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/schemas.md +0 -0
  35. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/telemetry.md +0 -0
  36. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/api/utils.md +0 -0
  37. {mt5cli-1.3.0 → mt5cli-1.3.1}/docs/index.md +0 -0
  38. {mt5cli-1.3.0 → mt5cli-1.3.1}/examples/grafana/README.md +0 -0
  39. {mt5cli-1.3.0 → mt5cli-1.3.1}/examples/grafana/compose.yml +0 -0
  40. {mt5cli-1.3.0 → mt5cli-1.3.1}/examples/grafana/dashboards/mt5cli-market.json +0 -0
  41. {mt5cli-1.3.0 → mt5cli-1.3.1}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
  42. {mt5cli-1.3.0 → mt5cli-1.3.1}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
  43. {mt5cli-1.3.0 → mt5cli-1.3.1}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
  44. {mt5cli-1.3.0 → mt5cli-1.3.1}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
  45. {mt5cli-1.3.0 → mt5cli-1.3.1}/mkdocs.yml +0 -0
  46. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/__init__.py +0 -0
  47. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/__main__.py +0 -0
  48. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/cli.py +0 -0
  49. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/client.py +0 -0
  50. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/contract.py +0 -0
  51. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/converters.py +0 -0
  52. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/exceptions.py +0 -0
  53. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/grafana.py +0 -0
  54. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/history.py +0 -0
  55. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/marketdata.py +0 -0
  56. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/observability.py +0 -0
  57. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/retry.py +0 -0
  58. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/schemas.py +0 -0
  59. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/telemetry.py +0 -0
  60. {mt5cli-1.3.0 → mt5cli-1.3.1}/mt5cli/utils.py +0 -0
  61. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/__init__.py +0 -0
  62. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/conftest.py +0 -0
  63. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_cli.py +0 -0
  64. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_client.py +0 -0
  65. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_contracts.py +0 -0
  66. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_examples.py +0 -0
  67. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_grafana.py +0 -0
  68. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_history.py +0 -0
  69. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_marketdata.py +0 -0
  70. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_observability.py +0 -0
  71. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_packaging.py +0 -0
  72. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_telemetry.py +0 -0
  73. {mt5cli-1.3.0 → mt5cli-1.3.1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 1.3.0
3
+ Version: 1.3.1
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>
@@ -269,6 +269,16 @@ updating symbol (paced by `sample_interval_seconds`) or across several
269
269
  symbols passed to the constructor. Any two matched samples that disagree
270
270
  abort the calibration (`offset_disagreement`).
271
271
 
272
+ The `copy_ticks_range()` query backing each sample extends its upper bound
273
+ `_MAX_FUTURE_SKEW_SECONDS` (2 minutes) past the host clock's current time, in
274
+ addition to covering the full trailing `copied_window_seconds`. A live tick's
275
+ true UTC event is not always at or before the host clock — feed/broker clock
276
+ skew can put it a few seconds ahead — and without that tolerance the matching
277
+ copied row would never be queried, so calibration would always report
278
+ `no_matching_event`. The tolerance is bounded, not open-ended: an event
279
+ further ahead than `_MAX_FUTURE_SKEW_SECONDS` is still excluded from the
280
+ query and cannot be matched.
281
+
272
282
  ### Failure modes and fail-closed behavior
273
283
 
274
284
  When calibration cannot be established safely — closed market or weekend
@@ -1060,7 +1060,14 @@ def _recent_copied_ticks(
1060
1060
  window_seconds: float,
1061
1061
  now_epoch: float,
1062
1062
  ) -> pd.DataFrame | None:
1063
- """Fetch copied ticks for the trailing UTC window ending now.
1063
+ """Fetch copied ticks for the trailing UTC window ending just past now.
1064
+
1065
+ The upper query bound extends ``_MAX_FUTURE_SKEW_SECONDS`` past
1066
+ ``now_epoch`` so a live tick whose true UTC event is slightly ahead of
1067
+ the host clock (feed/server clock skew, not a broker wall-clock label)
1068
+ still falls inside the queried range and can be matched; that tolerance
1069
+ matches the future-skew bound already enforced on normalized snapshots,
1070
+ so it cannot admit an event further ahead than the supported policy.
1064
1071
 
1065
1072
  Returns:
1066
1073
  The copied-tick DataFrame, or ``None`` when retrieval fails or the
@@ -1069,13 +1076,14 @@ def _recent_copied_ticks(
1069
1076
  flags = getattr(getattr(client, "mt5", None), "COPY_TICKS_ALL", None)
1070
1077
  if not isinstance(flags, int):
1071
1078
  flags = _COPY_TICKS_ALL_FALLBACK
1079
+ query_end_epoch = now_epoch + _MAX_FUTURE_SKEW_SECONDS
1072
1080
  try:
1073
1081
  frame = cast(
1074
1082
  "object",
1075
1083
  client.copy_ticks_range(
1076
1084
  symbol,
1077
1085
  datetime.fromtimestamp(now_epoch - window_seconds, tz=UTC),
1078
- datetime.fromtimestamp(now_epoch, tz=UTC),
1086
+ datetime.fromtimestamp(query_end_epoch, tz=UTC),
1079
1087
  flags,
1080
1088
  ),
1081
1089
  )
@@ -1242,7 +1250,10 @@ class TickClockNormalizer:
1242
1250
  sample_interval_seconds: Pause between consecutive samples so an
1243
1251
  actively updating symbol can produce distinct tick events.
1244
1252
  copied_window_seconds: Trailing UTC window queried from
1245
- ``copy_ticks_range`` per sample.
1253
+ ``copy_ticks_range`` per sample, ending
1254
+ ``_MAX_FUTURE_SKEW_SECONDS`` past the host clock's current
1255
+ time so a live tick slightly ahead of the host clock (feed
1256
+ clock skew) remains matchable.
1246
1257
  max_calibration_age_seconds: Age after which a cached calibration
1247
1258
  is recomputed unconditionally (bounds DST-transition
1248
1259
  staleness).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mt5cli"
3
- version = "1.3.0"
3
+ version = "1.3.1"
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"}]
@@ -40,6 +40,8 @@ Repository = "https://github.com/dceoy/mt5cli.git"
40
40
 
41
41
  [tool.uv]
42
42
  required-environments = ["platform_system == 'Windows'"]
43
+ exclude-newer = "1 week"
44
+ exclude-newer-package = { pdmt5 = false }
43
45
 
44
46
  [dependency-groups]
45
47
  dev = [
@@ -14,12 +14,15 @@ from numpy import int64 as np_int64
14
14
  from pdmt5 import Mt5RuntimeError
15
15
 
16
16
  if TYPE_CHECKING:
17
+ from collections.abc import Callable
18
+
17
19
  from pytest_mock import MockerFixture
18
20
 
19
21
  from mt5cli import trading
20
22
  from mt5cli.client import MT5Client
21
23
  from mt5cli.exceptions import Mt5OperationError
22
24
  from mt5cli.trading import (
25
+ _MAX_FUTURE_SKEW_SECONDS, # type: ignore[reportPrivateUsage]
23
26
  MarginVolume,
24
27
  OrderExecutionResult,
25
28
  OrderLimits,
@@ -4145,6 +4148,31 @@ def _clock_client(
4145
4148
  return client
4146
4149
 
4147
4150
 
4151
+ def _windowed_copy_ticks_range(
4152
+ frame: pd.DataFrame,
4153
+ ) -> Callable[[str, datetime, datetime, int], pd.DataFrame]:
4154
+ """Build a ``copy_ticks_range`` fake that filters rows by query bounds.
4155
+
4156
+ Real MT5 only returns rows within ``[date_from, date_to]``; a plain
4157
+ canned return value would hide a regression where the production code
4158
+ queries too narrow a window, since the mock would return the same rows
4159
+ regardless of the arguments it was called with.
4160
+ """
4161
+
4162
+ def _copy_ticks_range(
4163
+ _symbol: str,
4164
+ date_from: datetime,
4165
+ date_to: datetime,
4166
+ _flags: int,
4167
+ ) -> pd.DataFrame:
4168
+ from_epoch = date_from.timestamp()
4169
+ to_epoch = date_to.timestamp()
4170
+ mask = (frame["time"] >= from_epoch) & (frame["time"] <= to_epoch)
4171
+ return frame[mask].reset_index(drop=True)
4172
+
4173
+ return _copy_ticks_range
4174
+
4175
+
4148
4176
  class TestTickClockNormalizer:
4149
4177
  """Tests for TickClockNormalizer calibration and UTC normalization."""
4150
4178
 
@@ -4192,7 +4220,7 @@ class TestTickClockNormalizer:
4192
4220
  mt5_flag: int | None,
4193
4221
  expected_flag: int,
4194
4222
  ) -> None:
4195
- """Copied ticks are queried over a trailing UTC window ending now."""
4223
+ """Copied ticks are queried over a window trailing and past now."""
4196
4224
  _freeze_clock(mocker)
4197
4225
  event = _CLOCK_NOW_EPOCH - 1
4198
4226
  client = _clock_client(
@@ -4213,10 +4241,87 @@ class TestTickClockNormalizer:
4213
4241
  client.copy_ticks_range.assert_called_once_with(
4214
4242
  "SING30",
4215
4243
  datetime.fromtimestamp(_CLOCK_NOW_EPOCH - 300.0, tz=UTC),
4216
- datetime.fromtimestamp(_CLOCK_NOW_EPOCH, tz=UTC),
4244
+ datetime.fromtimestamp(
4245
+ _CLOCK_NOW_EPOCH + _MAX_FUTURE_SKEW_SECONDS,
4246
+ tz=UTC,
4247
+ ),
4217
4248
  expected_flag,
4218
4249
  )
4219
4250
 
4251
+ def test_live_tick_ahead_of_host_clock_matches_within_widened_window(
4252
+ self,
4253
+ mocker: MockerFixture,
4254
+ ) -> None:
4255
+ """A live tick ~16s ahead of the host clock still calibrates.
4256
+
4257
+ Reproduces the dceoy/mteor#428 production failure: the feed/server
4258
+ clock can run slightly ahead of the host OS clock, so the true UTC
4259
+ event backing a live tick can itself land a few seconds past
4260
+ ``datetime.now(UTC)``. Querying ``copy_ticks_range`` with
4261
+ ``date_to=now`` would exclude that event, leaving only an unrelated
4262
+ older row and forcing ``no_matching_event`` on every sample.
4263
+ """
4264
+ _freeze_clock(mocker)
4265
+ skew = 16.0
4266
+ true_utc_event = _CLOCK_NOW_EPOCH + skew
4267
+ older_unrelated_event = _CLOCK_NOW_EPOCH - 30
4268
+ copied = pd.concat(
4269
+ [
4270
+ _copied_frame(older_unrelated_event, bid=9.9, ask=9.9),
4271
+ _copied_frame(true_utc_event),
4272
+ ],
4273
+ ignore_index=True,
4274
+ )
4275
+ client = _clock_client(
4276
+ [_live_tick(true_utc_event + _UTC_PLUS_3)],
4277
+ pd.DataFrame(),
4278
+ )
4279
+ client.copy_ticks_range.side_effect = _windowed_copy_ticks_range(copied)
4280
+ normalizer = TickClockNormalizer(
4281
+ client,
4282
+ ["SING30"],
4283
+ samples_per_symbol=1,
4284
+ min_agreeing_samples=1,
4285
+ )
4286
+
4287
+ calibration = normalizer.calibrate()
4288
+
4289
+ assert calibration.status == "calibrated"
4290
+ _assert_close(calibration.offset_seconds, _UTC_PLUS_3)
4291
+ assert client.copy_ticks_range.call_count == 1
4292
+
4293
+ def test_copied_event_beyond_future_skew_policy_is_not_matched(
4294
+ self,
4295
+ mocker: MockerFixture,
4296
+ ) -> None:
4297
+ """An event further ahead than the tolerated skew is never queried.
4298
+
4299
+ The widened upper bound is bounded, not open-ended: it matches
4300
+ ``_MAX_FUTURE_SKEW_SECONDS``, the same policy enforced on normalized
4301
+ snapshots, so an event beyond that horizon is excluded from the
4302
+ copied-tick query and can never be silently accepted as a match.
4303
+ """
4304
+ _freeze_clock(mocker)
4305
+ beyond_skew_event = _CLOCK_NOW_EPOCH + _MAX_FUTURE_SKEW_SECONDS + 1.0
4306
+ client = _clock_client(
4307
+ [_live_tick(beyond_skew_event + _UTC_PLUS_3)],
4308
+ pd.DataFrame(),
4309
+ )
4310
+ client.copy_ticks_range.side_effect = _windowed_copy_ticks_range(
4311
+ _copied_frame(beyond_skew_event),
4312
+ )
4313
+ normalizer = TickClockNormalizer(
4314
+ client,
4315
+ ["SING30"],
4316
+ samples_per_symbol=1,
4317
+ min_agreeing_samples=1,
4318
+ )
4319
+
4320
+ calibration = normalizer.calibrate()
4321
+
4322
+ assert calibration.status == "no_copied_ticks"
4323
+ assert calibration.offset_seconds is None
4324
+
4220
4325
  def test_matches_older_copied_row_when_newest_row_disagrees(
4221
4326
  self,
4222
4327
  mocker: MockerFixture,
@@ -10,6 +10,13 @@ required-markers = [
10
10
  "sys_platform == 'win32'",
11
11
  ]
12
12
 
13
+ [options]
14
+ exclude-newer = "2026-07-13T15:37:13.721821839Z"
15
+ exclude-newer-span = "P1W"
16
+
17
+ [options.exclude-newer-package]
18
+ pdmt5 = false
19
+
13
20
  [[package]]
14
21
  name = "annotated-doc"
15
22
  version = "0.0.4"
@@ -499,7 +506,7 @@ wheels = [
499
506
 
500
507
  [[package]]
501
508
  name = "mt5cli"
502
- version = "1.3.0"
509
+ version = "1.3.1"
503
510
  source = { editable = "." }
504
511
  dependencies = [
505
512
  { name = "pandas" },
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