mt5cli 1.3.2__tar.gz → 1.4.0__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.2 → mt5cli-1.4.0}/PKG-INFO +1 -1
  2. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/public-contract.md +1 -1
  3. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/trading.md +111 -45
  4. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/trading.py +642 -272
  5. {mt5cli-1.3.2 → mt5cli-1.4.0}/pyproject.toml +1 -1
  6. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_trading.py +1659 -723
  7. {mt5cli-1.3.2 → mt5cli-1.4.0}/uv.lock +2 -2
  8. {mt5cli-1.3.2 → mt5cli-1.4.0}/.agents/skills/local-qa/SKILL.md +0 -0
  9. {mt5cli-1.3.2 → mt5cli-1.4.0}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  10. {mt5cli-1.3.2 → mt5cli-1.4.0}/.agents/skills/mt5cli/SKILL.md +0 -0
  11. {mt5cli-1.3.2 → mt5cli-1.4.0}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
  12. {mt5cli-1.3.2 → mt5cli-1.4.0}/.claude/agents/codex.md +0 -0
  13. {mt5cli-1.3.2 → mt5cli-1.4.0}/.claude/settings.json +0 -0
  14. {mt5cli-1.3.2 → mt5cli-1.4.0}/.github/FUNDING.yml +0 -0
  15. {mt5cli-1.3.2 → mt5cli-1.4.0}/.github/dependabot.yml +0 -0
  16. {mt5cli-1.3.2 → mt5cli-1.4.0}/.github/renovate.json +0 -0
  17. {mt5cli-1.3.2 → mt5cli-1.4.0}/.github/workflows/ci.yml +0 -0
  18. {mt5cli-1.3.2 → mt5cli-1.4.0}/.github/workflows/claude.yml +0 -0
  19. {mt5cli-1.3.2 → mt5cli-1.4.0}/.github/workflows/release.yml +0 -0
  20. {mt5cli-1.3.2 → mt5cli-1.4.0}/.gitignore +0 -0
  21. {mt5cli-1.3.2 → mt5cli-1.4.0}/AGENTS.md +0 -0
  22. {mt5cli-1.3.2 → mt5cli-1.4.0}/CLAUDE.md +0 -0
  23. {mt5cli-1.3.2 → mt5cli-1.4.0}/LICENSE +0 -0
  24. {mt5cli-1.3.2 → mt5cli-1.4.0}/README.md +0 -0
  25. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/cli.md +0 -0
  26. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/client.md +0 -0
  27. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/converters.md +0 -0
  28. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/exceptions.md +0 -0
  29. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/grafana.md +0 -0
  30. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/history.md +0 -0
  31. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/index.md +0 -0
  32. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/marketdata.md +0 -0
  33. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/observability.md +0 -0
  34. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/schemas.md +0 -0
  35. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/telemetry.md +0 -0
  36. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/api/utils.md +0 -0
  37. {mt5cli-1.3.2 → mt5cli-1.4.0}/docs/index.md +0 -0
  38. {mt5cli-1.3.2 → mt5cli-1.4.0}/examples/grafana/README.md +0 -0
  39. {mt5cli-1.3.2 → mt5cli-1.4.0}/examples/grafana/compose.yml +0 -0
  40. {mt5cli-1.3.2 → mt5cli-1.4.0}/examples/grafana/dashboards/mt5cli-market.json +0 -0
  41. {mt5cli-1.3.2 → mt5cli-1.4.0}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
  42. {mt5cli-1.3.2 → mt5cli-1.4.0}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
  43. {mt5cli-1.3.2 → mt5cli-1.4.0}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
  44. {mt5cli-1.3.2 → mt5cli-1.4.0}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
  45. {mt5cli-1.3.2 → mt5cli-1.4.0}/mkdocs.yml +0 -0
  46. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/__init__.py +0 -0
  47. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/__main__.py +0 -0
  48. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/cli.py +0 -0
  49. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/client.py +0 -0
  50. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/contract.py +0 -0
  51. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/converters.py +0 -0
  52. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/exceptions.py +0 -0
  53. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/grafana.py +0 -0
  54. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/history.py +0 -0
  55. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/marketdata.py +0 -0
  56. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/observability.py +0 -0
  57. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/retry.py +0 -0
  58. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/schemas.py +0 -0
  59. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/telemetry.py +0 -0
  60. {mt5cli-1.3.2 → mt5cli-1.4.0}/mt5cli/utils.py +0 -0
  61. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/__init__.py +0 -0
  62. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/conftest.py +0 -0
  63. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_cli.py +0 -0
  64. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_client.py +0 -0
  65. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_contracts.py +0 -0
  66. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_examples.py +0 -0
  67. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_grafana.py +0 -0
  68. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_history.py +0 -0
  69. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_marketdata.py +0 -0
  70. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_observability.py +0 -0
  71. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_packaging.py +0 -0
  72. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_telemetry.py +0 -0
  73. {mt5cli-1.3.2 → mt5cli-1.4.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 1.3.2
3
+ Version: 1.4.0
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>
@@ -102,7 +102,7 @@ strategy entries, exits, Kelly sizing, or signal logic.
102
102
  | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
103
103
  | `get_account_snapshot`, `get_symbol_snapshot`, `get_tick_snapshot`, `get_positions_frame` | Normalized account/symbol/tick/position views |
104
104
  | `extract_tick_price` | Positive finite bid/ask extraction from tick mappings |
105
- | `TickClockNormalizer` | Connection-scoped, copied-tick-validated UTC tick normalization |
105
+ | `TickClockNormalizer` | Connection-scoped, host-clock-validated UTC tick normalization |
106
106
  | `TickClockCalibration` | Server-clock calibration diagnostics (offset, evidence, status) |
107
107
  | `NormalizedTickSnapshot` | Typed latest-tick contract with raw vs validated UTC timestamps |
108
108
  | `CalibrationStatus`, `ClockStatus` | Literal status types for clock calibration and normalization |
@@ -204,16 +204,27 @@ provide entry-deal classification, Kelly sizing, or any betting-specific helpers
204
204
 
205
205
  ## Timestamp sources and broker clock offsets
206
206
 
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.
207
+ MetaQuotes documents `copy_ticks_range()` and `copy_rates_range()` results,
208
+ and the date bounds they accept, as UTC, and does not document a different
209
+ timezone contract for `symbol_info_tick()`. In production on at least one
210
+ OANDA-style broker/terminal, however, `symbol_info_tick()` returned epochs
211
+ carrying the broker's own wall-clock label (UTC+3), not true UTC: treating
212
+ `copy_ticks_range()` as an independent UTC reference for the live tick clock
213
+ was the direct cause of a production calibration failure (see
214
+ [dceoy/mteor#428](https://github.com/dceoy/mteor/issues/428)), because a
215
+ query window built from the host clock missed the live event entirely, off
216
+ by exactly the broker's offset. mt5cli does not call `copy_ticks_range()` or
217
+ `copy_rates_range()` to calibrate the live tick clock, and does not
218
+ independently verify their documented UTC contract on any broker or
219
+ terminal — treat copied-history timestamps as broker/terminal-dependent
220
+ absent evidence from your own broker/terminal combination.
211
221
 
212
222
  `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.
223
+ not expose pdmt5's timezone-naive `Timestamp` conversion or alter the instant,
224
+ and it never applies any offset correction. Any broker-specific offset
225
+ handling requires independent evidence — see `TickClockNormalizer` below —
226
+ and must be applied separately. Do not subtract a presumed UTC+2 or UTC+3
227
+ offset from a raw timestamp without that evidence.
217
228
 
218
229
  No offset is applied automatically by history retrieval helpers.
219
230
 
@@ -251,47 +262,60 @@ A normalized snapshot keeps the raw/UTC distinction explicit:
251
262
 
252
263
  ### Calibration evidence
253
264
 
254
- The offset is never inferred from `latest_tick_time - now` alone. Each
255
- calibration sample fetches the live `symbol_info_tick()` value **and** recent
256
- UTC-labeled `copy_ticks_range()` data, then searches every recent copied row
257
- (not just the newest) for one that matches the live tick on its shared
258
- positive `bid`/`ask`/`last` fields. Volume is not used as event identity
259
- because latest-tick and copied-tick views can represent OTC quote volume
260
- differently. The newest matching row sets the resulting offset, which must
261
- align to a 30-minute increment within the realistic UTC-14..UTC+14 range.
262
- `time_msc` is preferred over second-resolution `time` on both sides when
263
- available.
265
+ `copy_ticks_range()` is not used for calibration: its documented UTC contract
266
+ is broker/terminal-dependent and not independently verified here (see
267
+ above), so neither the query timestamps nor the returned historical tick
268
+ timestamps can serve as an out-of-band UTC reference, and historical copied
269
+ rows are never used to infer the server offset. Instead, the only
270
+ independent reference available is **this process's own clock**: every
271
+ calibration poll
272
+ fetches one live `symbol_info_tick()` value and stamps it with
273
+ `datetime.now(UTC)` at the moment it is received. Comparing that host receipt
274
+ time against the tick's own server-labeled epoch yields one candidate offset,
275
+ which must align to a 30-minute increment within the realistic UTC-14..UTC+14
276
+ range. `time_msc` is preferred over second-resolution `time` when available.
277
+
278
+ A single poll is never trusted on its own. A closed, stale, or illiquid
279
+ symbol keeps returning the exact same last-traded tick, so its epoch never
280
+ changes between polls — that ordinary tick age is not calibration evidence.
281
+ Only a poll whose epoch _differs_ from the previous poll (comparison is
282
+ inequality, not "must increase": a genuine broker offset **decrease**, e.g. a
283
+ UTC+3 to UTC+2 DST fallback, relabels the next fresh event with a numerically
284
+ _smaller_ epoch than the last one observed under the old, larger offset)
285
+ proves the broker produced a new event, and only then is its offset computed.
264
286
 
265
287
  An offset is accepted only after `min_agreeing_samples` (default 2)
266
- **distinct** matched tick events agree on the same rounded offset — repeated
267
- observations of one unchanged tick never count twice, so an illiquid symbol
288
+ **distinct** fresh tick events agree on the same rounded offset — repeated
289
+ polls of one unchanged tick never count as evidence, so an illiquid symbol
268
290
  cannot self-confirm. Evidence can come from multiple samples of one actively
269
291
  updating symbol (paced by `sample_interval_seconds`) or across several
270
- symbols passed to the constructor. Any two matched samples that disagree
271
- abort the calibration (`offset_disagreement`).
272
-
273
- The `copy_ticks_range()` query backing each sample extends its upper bound
274
- `_MAX_FUTURE_SKEW_SECONDS` (2 minutes) past the host clock's current time, in
275
- addition to covering the full trailing `copied_window_seconds`. A live tick's
276
- true UTC event is not always at or before the host clock — feed/broker clock
277
- skew can put it a few seconds ahead — and without that tolerance the matching
278
- copied row would never be queried, so calibration would always report
279
- `no_matching_event`. The tolerance is bounded, not open-ended: an event
280
- further ahead than `_MAX_FUTURE_SKEW_SECONDS` is still excluded from the
281
- query and cannot be matched.
292
+ symbols passed to the constructor; a closed symbol among several configured
293
+ symbols simply contributes no evidence rather than blocking the others. Any
294
+ two fresh events that disagree on the rounded offset abort the calibration
295
+ (`offset_disagreement`) instead of being averaged or resolved by recency.
282
296
 
283
297
  ### Failure modes and fail-closed behavior
284
298
 
285
- When calibration cannot be established safely — closed market or weekend
286
- (`no_copied_ticks`), price disagreement or a non-half-hour delta
287
- (`no_matching_event`), a single matched event only (`insufficient_agreement`),
288
- missing live data (`no_live_tick`), or an unrealistic delta
289
- (`implausible_offset`) — snapshots fail closed: `clock_status` is
299
+ When calibration cannot be established safely — no symbol ever produced a
300
+ fresh (changed-epoch) tick, e.g. every configured symbol is closed or
301
+ illiquid (`not_advancing`), a fresh event's raw offset does not round cleanly
302
+ to a plausible bucket (`unstable_offset`), an unrealistic rounded delta
303
+ (`implausible_offset`), too few distinct agreeing events
304
+ (`insufficient_agreement`), disagreeing events (`offset_disagreement`), or
305
+ missing live data (`no_live_tick`) — snapshots fail closed: `clock_status` is
290
306
  `"uncalibrated"`, `time_utc` and `server_clock_offset_seconds` are `None`,
291
307
  and the raw fields remain available. A raw timestamp is never silently
292
- treated as UTC. `normalizer.calibration` exposes the full
308
+ treated as UTC. The same fail-closed rule applies after a successful
309
+ calibration: once periodic revalidation invalidates the cached offset (see
310
+ below), subsequent snapshots are normalized only by a successful full
311
+ recalibration — never by the refuted cache. `normalizer.calibration`
312
+ exposes the full
293
313
  `TickClockCalibration` diagnostics: status, offset, distinct sample count,
294
- evidence symbols, and last calibration time (`to_dict()` for serialization).
314
+ evidence symbols, last calibration time, and (for troubleshooting a failed
315
+ attempt) the most recently considered sample's symbol, raw server-labeled
316
+ time, host observation time, and raw (pre-rounding) offset —
317
+ `to_dict()` for serialization, and all of it is included in the one
318
+ concise warning logged per failed calibration attempt.
295
319
 
296
320
  ### Caching and revalidation
297
321
 
@@ -305,12 +329,54 @@ symbols and calls. A cached offset is recomputed in three cases:
305
329
  future — evidence that the broker offset grew, e.g. a UTC+2 to UTC+3
306
330
  transition.
307
331
  - Periodically, at most once per `revalidation_interval_seconds` (default 5
308
- minutes), configured symbols are sampled until one confirms the
309
- still-cached offset. Closed or inconclusive symbols are skipped; a detected
310
- disagreement forces full recalibration. This is what catches a broker
311
- offset _decrease_ (e.g. UTC+3 to UTC+2): the resulting normalized time
312
- looks stale rather than future, which a future-skew check alone cannot
313
- distinguish from ordinary quiet-market staleness.
332
+ minutes), through one full revalidation round over every configured
333
+ symbol. This is what catches a broker offset _decrease_ (e.g. UTC+3 to
334
+ UTC+2): the resulting normalized time looks stale rather than future,
335
+ which a future-skew check alone cannot distinguish from ordinary
336
+ quiet-market staleness.
337
+
338
+ In a revalidation round each configured symbol is polled once and compared
339
+ against that symbol's last known observation (from the initial calibration
340
+ or a prior revalidation), and the whole round is evaluated before any
341
+ decision is made — revalidation never stops at the first confirming symbol.
342
+ A symbol is inconclusive — skipped, neither confirming nor refuting the
343
+ cache — when its fetch is unusable, when it has no prior observation to
344
+ compare against, or when its tick epoch is unchanged since that observation
345
+ (ordinary tick age, e.g. a closed or illiquid symbol). Every other advancing
346
+ sample is evidence the cache must be able to explain:
347
+
348
+ - an accepted offset equal to the cached one confirms the cache, but never
349
+ cancels another symbol's refuting evidence from the same round;
350
+ - an accepted offset that differs from the cached one refutes the cache (a
351
+ clean broker offset change) — unless the cached offset, widened by the
352
+ elapsed time since that symbol's prior observation plus that prior
353
+ observation's own inferred delay under the cached offset, still explains
354
+ the disagreeing raw offset. That case is forgiven once per symbol and held as
355
+ a pending disagreement instead of refuting immediately, since it cannot
356
+ yet be told apart from a maximally delayed tick under the cached offset.
357
+ A pending disagreement escalates to a refutation only if that same
358
+ symbol's next conclusive round rounds to the identical bucket again; any
359
+ inconclusive, differently-bucketed, or unusable round in between clears
360
+ it instead of letting unrelated samples accumulate into false
361
+ corroboration;
362
+ - an advancing sample with no usable offset (`unstable_offset` or
363
+ `implausible_offset`) also refutes the cache, subject to the same
364
+ once-per-symbol forgiveness above — but an unusable sample can never
365
+ itself be held as, or match, a pending disagreement, since it carries no
366
+ specific offset to corroborate. A fresh market event whose raw offset is
367
+ neither reconciled with any plausible 30-minute bucket nor forgiven this
368
+ way is contradictory evidence the cached calibration cannot explain.
369
+
370
+ Any refuting evidence invalidates the cached calibration and forces full
371
+ recalibration; when several symbols refute the cache in one round, the
372
+ reported evidence is deterministic (`implausible_offset` first, then
373
+ `unstable_offset`, then a clean disagreement). The recalibration either
374
+ re-establishes a validated offset or fails closed — a raw tick is never
375
+ normalized with a cached offset that advancing evidence has just refuted. A
376
+ wholly inconclusive round keeps the cache, and a round in which no symbol
377
+ had a comparable baseline does not consume the interval, so the next call
378
+ retries immediately instead of waiting a full
379
+ `revalidation_interval_seconds`.
314
380
 
315
381
  If recalibration still cannot validate the timestamp, the snapshot fails
316
382
  closed. Failed calibrations are recorded for diagnostics but never reused;