* feat: Grafana copy publishing, dashboard examples, and optional OTel metrics
Implements three observability improvements:
#82 — publish_grafana_copy(): Uses SQLite online backup API (WAL-safe) to
atomically publish a consistent read-only copy beside the target. Adds
--publish-copy option to grafana-schema and snapshot CLI commands.
#83 — examples/grafana/: Minimal working Grafana setup with docker-compose,
provisioning datasource/dashboard YAML, and three dashboard JSON files
(mt5cli-overview, mt5cli-trades, mt5cli-market). All queries use grafana_*
views; no credentials or private paths included.
#84 — mt5cli/telemetry.py: Optional OTel metrics behind mt5cli[otel] extra.
Base install is unaffected. Adds _Mt5Metrics singleton (no-op until
configure_metrics() is called), wraps update_history() and
update_observability() with record_history_update / record_snapshot_update
context managers, and emits account/position gauges from snapshots.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: replace ambiguous multiplication sign in comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: normalize markdown formatting in grafana README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: preserve file mode on Grafana copy and fix unsupported time macro
- publish_grafana_copy: chmod temp file to match the existing target's
permissions (or 0o644 when no prior target exists) before atomic
replace, so Grafana running as a different OS user (e.g. UID 472 in
Docker) can read the published database
- mt5cli-market.json: replace unsupported \$__timeFilter(time) with the
epoch-based filter supported by frser-sqlite-datasource:
"time" >= \$__from / 1000 AND "time" < \$__to / 1000
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: skip Windows-incompatible mode test, rename compose file to compose.yaml
- Skip test_overwrite_preserves_existing_target_mode on win32 since
Windows chmod does not preserve Unix group/other permission bits
- Simplify test_fresh_target_has_readable_permissions to check owner
read bit only (portable across platforms)
- Rename docker-compose.yml -> compose.yaml (modern Compose convention)
- Update README and test reference to match new filename
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: rename *.yaml to *.yml in examples/grafana
Renames compose.yaml, mt5cli-sqlite.yaml, and mt5cli.yaml to .yml;
updates README and test references accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: format Grafana dashboards and expand qa script to include JSON
- Update qa.sh prettier pattern to format JSON files alongside markdown
- Reformat Grafana dashboard JSONs with consistent spacing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address owner review comments before merge
- qa.sh: fix Prettier glob from `{,d,json}` to `{md,json}` so Markdown
files are actually formatted by local QA (P2)
- compose.yml: add GF_INSTALL_PLUGINS env var so the frser-sqlite-datasource
plugin is installed at container start (P1)
- telemetry.py: replace no-op get_meter() call with a real SDK MeterProvider
pipeline; add optional `readers` kwarg so callers can inject custom readers
(e.g. InMemoryMetricReader in tests) without needing the OTLP package (P1)
- sdk.py: aggregate profit and volume by symbol before emitting gauge values
so hedging accounts with multiple same-symbol positions emit one point per
symbol instead of overwriting with each row (P2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: emit mt5_history_update_rows_total via conn.total_changes delta
The counter was registered but never incremented, making the advertised
history-update throughput metric permanently zero. Add add_history_rows()
to _Mt5Metrics and call it in update_history() using the SQLite
total_changes delta measured around write_incremental_datasets().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address three owner review comments
- compose.yml: replace soft fallback with :? error expansion so Compose
refuses to start when MT5CLI_DB_PATH is unset or empty (P1)
- README.md: tell native Windows users to copy only the datasource
provisioning file; the dashboards yml contains a Docker-specific path
that is invalid on Windows (P2)
- telemetry.py / sdk.py: emit mt5_terminal_connected,
mt5_terminal_trade_allowed, and mt5_terminal_trade_expert gauges via a
new record_terminal_state() method called from _snapshot_terminal(),
completing the connection-status metric surface from issue #84 (P2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add snapshot freshness panel and win-rate column to dashboards
- mt5cli-overview.json: add a full-width "Last Snapshot" stat panel
(dateTimeFromNow unit) below the account stats, querying
MAX(time)*1000 from grafana_account_snapshots so users can tell
whether Grafana is reading a current published copy (#83)
- mt5cli-trades.json: add win_rate_pct computed column to the Trade
Statistics by Symbol table via 100.0 * winning_deals / NULLIF(
total_deals, 0), with a percent unit override and "Win Rate (%)"
display label (#83)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: reject same source and target path in publish_grafana_copy
Adds an early same-path guard to publish_grafana_copy: resolves both
paths before any I/O and raises ValueError if they are identical,
preventing the function from overwriting the live source database with
its own backup copy. Also adds a unit test for the rejected case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address ruff EM102/TRY003/E501 in same-path guard
Assigns the ValueError message to a variable before raising and
shortens the test docstring to stay within the 88-char line limit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: apply ruff format to publish_grafana_copy error message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove grafana_ticks panel from default market dashboard
The Tick Bid/Ask panel queried grafana_ticks which only exists when users
collect tick data (opt-in). Users following the default OHLCV-only setup
path hit "no such table: grafana_ticks" on dashboard load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: close SQLite connections before atomic replace in publish_grafana_copy
Wrap both src and dst connections with contextlib.closing() so they are
explicitly closed before tmp_path.replace(target_path) runs. Without
this, sqlite3.Connection's context manager only commits/rolls back but
leaves the file handle open, which can cause PermissionError on Windows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: rename history.grafana.db to history.mt5cli.db in Grafana examples
frser-sqlite-datasource blocks paths containing "grafana.db" via its
internal blocklist. Rename the recommended published filename in the
README, compose comment, and datasource provisioning comment to avoid
a blocked/denied datasource for native Windows users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: update Docker Compose quick-start to pass MT5CLI_DB_PATH
The compose.yml already required MT5CLI_DB_PATH via ${MT5CLI_DB_PATH:?...},
but the README still showed bare `docker compose up -d`. Update the section
to show the env-var-prefixed invocation and document the .env file alternative.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: agent <agent@localhost>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add fetch_latest_closed_rates_indexed and allow_whole_dollar_env opt-in (#43, #44)
Closes#43: add fetch_latest_closed_rates_indexed(client, *, symbol,
granularity, count) -> pd.DataFrame to mt5cli/trading.py. Internally
reuses fetch_latest_closed_rates_for_trading_client(), converts the
"time" column to a UTC-aware DatetimeIndex named "time", and drops the
original column. Exported from trading.__all__, mt5cli.__init__, and
STABLE_SDK_EXPORTS.
Closes#44: extend substitute_env_placeholders() with opt-in
allow_whole_dollar_env=False that expands whole-value $ENV_NAME strings
(entire string must be exactly $IDENTIFIER). Threaded through
build_config(), resolve_account_spec(), and resolve_account_specs() with
the same default=False. Partial strings like "plan$pass", "abc$ENV", or
"$ENV-suffix" are never expanded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: align Markdown table columns in docs and skill file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: treat numeric (float64) epoch seconds as UTC in _rate_time_to_utc
After DataFrame concat or NA upcast the time column becomes float64, which
is still epoch seconds. Using is_numeric_dtype instead of is_integer_dtype
fixes the silent misalignment. Using series.to_numpy() before passing to
pd.to_datetime avoids the redundant pd.DatetimeIndex() wrapper and aligns
with how existing rate-time normalization in schemas.py handles numeric
timestamps.
Add test_converts_float_epoch_seconds_to_utc_datetime_index to cover the
regression. Add a doc note clarifying that build_config cannot expand
login since that parameter is int | None.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: reject NaT values after rate timestamp conversion in _rate_time_to_utc
pd.to_datetime() silently produces NaT for None/NaN inputs rather than
raising, so the function could return a DatetimeIndex containing NaT
despite documenting invalid timestamps as a ValueError. Check any(idx.isna())
after conversion and raise with a clear message.
Add test_raises_on_nat_time_column to cover the regression.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Bump version to v0.9.0
* fix: handle object numeric rate timestamps
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add mt5cli agent skill
Document mt5cli CLI usage (global options, subcommands, parameter formats,
and examples) so agents can invoke the exporter without re-reading the CLI
source.
* Move mt5cli skill to top-level skills/ with symlink
Mirror the conventional layout: keep the canonical SKILL.md under
skills/mt5cli/ and expose it through a .agents/skills/mt5cli symlink.
* Harden mt5cli skill against leaking --password
Add a guideline warning against passing --password on the command line
(visible in ps/history/logs), and drop the credential-inline example that
modeled the bad pattern.
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add MkDocs documentation with API reference for the CLI module
- Add CI/CD and Claude Code review GitHub Actions workflows
- Add Dependabot and Renovate configuration for dependency updates
- Add .claude settings, agents, and local-qa skill with QA script
- Add AGENTS.md with repository guidelines and CLAUDE.md symlink
- Update README.md with installation, usage examples, and commands
https://claude.ai/code/session_01YW3YHru8wRH9dvHnBX7xf1