mt5cli 1.2.0__tar.gz → 1.2.2__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.0 → mt5cli-1.2.2}/.gitignore +3 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/PKG-INFO +1 -2
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/__init__.py +3 -64
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/cli.py +6 -38
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/grafana.py +70 -119
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/history.py +46 -16
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/marketdata.py +9 -6
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/observability.py +2 -10
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/telemetry.py +136 -101
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/trading.py +4 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/pyproject.toml +4 -2
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_cli.py +6 -4
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_history.py +14 -3
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_telemetry.py +21 -20
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_trading.py +34 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/uv.lock +1 -3
- mt5cli-1.2.0/.github/workflows/opencode.yml +0 -72
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.claude/agents/codex.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.claude/settings.json +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.github/FUNDING.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.github/dependabot.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.github/renovate.json +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.github/workflows/ci.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.github/workflows/claude.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/.github/workflows/release.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/AGENTS.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/CLAUDE.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/LICENSE +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/README.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/cli.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/client.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/converters.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/exceptions.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/grafana.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/history.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/index.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/marketdata.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/observability.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/public-contract.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/schemas.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/telemetry.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/trading.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/api/utils.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/docs/index.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/examples/grafana/README.md +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/examples/grafana/compose.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/examples/grafana/dashboards/mt5cli-market.json +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/examples/grafana/dashboards/mt5cli-overview.json +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/examples/grafana/dashboards/mt5cli-trades.json +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/examples/grafana/provisioning/dashboards/mt5cli.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/examples/grafana/provisioning/datasources/mt5cli-sqlite.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mkdocs.yml +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/__main__.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/client.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/contract.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/converters.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/exceptions.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/retry.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/schemas.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/mt5cli/utils.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/__init__.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/conftest.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_client.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_contracts.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_examples.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_grafana.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_marketdata.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_observability.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_packaging.py +0 -0
- {mt5cli-1.2.0 → mt5cli-1.2.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
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>
|
|
@@ -19,7 +19,6 @@ Requires-Python: <3.14,>=3.11
|
|
|
19
19
|
Requires-Dist: click>=8.1.0
|
|
20
20
|
Requires-Dist: pandas>=2.2.2
|
|
21
21
|
Requires-Dist: pdmt5>=1.2.0
|
|
22
|
-
Requires-Dist: pydantic>=2.13.4
|
|
23
22
|
Requires-Dist: typer>=0.15.0
|
|
24
23
|
Provides-Extra: otel
|
|
25
24
|
Requires-Dist: opentelemetry-api; extra == 'otel'
|
|
@@ -5,6 +5,7 @@ rather than private submodule helpers. See ``docs/api/public-contract.md`` for
|
|
|
5
5
|
the stable SDK contract, CLI surface, internal modules, and out-of-scope
|
|
6
6
|
strategy responsibilities.
|
|
7
7
|
"""
|
|
8
|
+
# pyright: reportUnusedImport=false, reportUnsupportedDunderAll=false
|
|
8
9
|
|
|
9
10
|
from importlib.metadata import version
|
|
10
11
|
|
|
@@ -80,69 +81,7 @@ from .trading import (
|
|
|
80
81
|
|
|
81
82
|
__version__ = version(__package__) if __package__ else None
|
|
82
83
|
|
|
83
|
-
__all__ = [
|
|
84
|
+
__all__ = [ # noqa: PLE0604
|
|
84
85
|
"STABLE_SDK_EXPORTS",
|
|
85
|
-
|
|
86
|
-
"ExecutionStatus",
|
|
87
|
-
"MT5Client",
|
|
88
|
-
"MarginVolume",
|
|
89
|
-
"Mt5CliError",
|
|
90
|
-
"Mt5ConnectionError",
|
|
91
|
-
"Mt5OperationError",
|
|
92
|
-
"Mt5SchemaError",
|
|
93
|
-
"OrderExecutionResult",
|
|
94
|
-
"OrderFillingMode",
|
|
95
|
-
"OrderLimits",
|
|
96
|
-
"OrderSide",
|
|
97
|
-
"OrderTimeMode",
|
|
98
|
-
"PositionSide",
|
|
99
|
-
"ProjectionMode",
|
|
100
|
-
"RateTarget",
|
|
101
|
-
"ThrottledHistoryUpdater",
|
|
102
|
-
"build_config",
|
|
103
|
-
"build_rate_targets",
|
|
104
|
-
"calculate_account_projected_margin_ratio",
|
|
105
|
-
"calculate_margin_and_volume",
|
|
106
|
-
"calculate_new_position_margin_ratio",
|
|
107
|
-
"calculate_positions_margin",
|
|
108
|
-
"calculate_positions_margin_by_symbol",
|
|
109
|
-
"calculate_positions_margin_safe",
|
|
110
|
-
"calculate_projected_margin_ratio",
|
|
111
|
-
"calculate_spread_ratio",
|
|
112
|
-
"calculate_symbol_group_margin_ratio",
|
|
113
|
-
"calculate_trailing_stop_updates",
|
|
114
|
-
"calculate_volume_by_margin",
|
|
115
|
-
"close_open_positions",
|
|
116
|
-
"collect_history",
|
|
117
|
-
"collect_latest_closed_rates_by_granularity",
|
|
118
|
-
"collect_latest_closed_rates_for_accounts",
|
|
119
|
-
"collect_latest_rates_for_accounts_with_retries",
|
|
120
|
-
"detect_position_side",
|
|
121
|
-
"determine_order_limits",
|
|
122
|
-
"drop_forming_rate_bar",
|
|
123
|
-
"ensure_symbol_selected",
|
|
124
|
-
"estimate_order_margin",
|
|
125
|
-
"estimate_server_clock_offset_seconds",
|
|
126
|
-
"extract_tick_price",
|
|
127
|
-
"fetch_latest_closed_rates",
|
|
128
|
-
"fetch_latest_closed_rates_indexed",
|
|
129
|
-
"get_account_snapshot",
|
|
130
|
-
"get_positions_frame",
|
|
131
|
-
"get_symbol_snapshot",
|
|
132
|
-
"get_tick_snapshot",
|
|
133
|
-
"load_rate_series_by_granularity",
|
|
134
|
-
"load_rate_series_from_sqlite",
|
|
135
|
-
"mt5_session",
|
|
136
|
-
"normalize_order_volume",
|
|
137
|
-
"place_market_order",
|
|
138
|
-
"report_rate_gaps",
|
|
139
|
-
"resolve_account_spec",
|
|
140
|
-
"resolve_account_specs",
|
|
141
|
-
"resolve_broker_filling_mode",
|
|
142
|
-
"update_history",
|
|
143
|
-
"update_history_with_config",
|
|
144
|
-
"update_observability",
|
|
145
|
-
"update_observability_with_config",
|
|
146
|
-
"update_sltp_for_open_positions",
|
|
147
|
-
"update_trailing_stop_loss_for_open_positions",
|
|
86
|
+
*sorted(STABLE_SDK_EXPORTS),
|
|
148
87
|
]
|
|
@@ -4,8 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
|
-
import os
|
|
8
|
-
import re
|
|
9
7
|
from contextlib import closing
|
|
10
8
|
from dataclasses import dataclass
|
|
11
9
|
from datetime import datetime
|
|
@@ -19,9 +17,9 @@ import typer
|
|
|
19
17
|
from .client import MT5Client, build_config, mt5_session
|
|
20
18
|
from .history import collect_history as _collect_history
|
|
21
19
|
from .history import (
|
|
20
|
+
infer_rate_table_granularity_seconds,
|
|
22
21
|
open_existing_sqlite_database,
|
|
23
22
|
report_rate_gaps,
|
|
24
|
-
resolve_granularity_name,
|
|
25
23
|
)
|
|
26
24
|
from .observability import update_observability_with_config
|
|
27
25
|
from .trading import OrderExecutionResult, close_open_positions
|
|
@@ -206,40 +204,12 @@ _CLI_ENV_DEFAULTS: dict[str, str] = {
|
|
|
206
204
|
"password": "MT5_PASSWORD",
|
|
207
205
|
"server": "MT5_SERVER",
|
|
208
206
|
}
|
|
209
|
-
_RATE_VIEW_NAME_RE = re.compile(
|
|
210
|
-
r"^rate_(?P<symbol>.+)__(?:(?P<granularity>[A-Z0-9]+)_)?(?P<timeframe>\d+)$",
|
|
211
|
-
)
|
|
212
207
|
|
|
213
208
|
|
|
214
209
|
def _get_export_context(ctx: typer.Context) -> _ExportContext:
|
|
215
210
|
return cast("_ExportContext", ctx.obj)
|
|
216
211
|
|
|
217
212
|
|
|
218
|
-
def _resolve_cli_option(value: str | None, env_name: str) -> str | None:
|
|
219
|
-
return value if value is not None else os.environ.get(env_name)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def _timeframe_interval_seconds(timeframe: int) -> int | None:
|
|
223
|
-
granularity = resolve_granularity_name(timeframe)
|
|
224
|
-
units = {
|
|
225
|
-
"M": 60,
|
|
226
|
-
"H": 3600,
|
|
227
|
-
"D": 86400,
|
|
228
|
-
"W": 604800,
|
|
229
|
-
}
|
|
230
|
-
for prefix, seconds in units.items():
|
|
231
|
-
suffix = granularity.removeprefix(prefix)
|
|
232
|
-
if granularity.startswith(prefix) and suffix.isdigit():
|
|
233
|
-
return int(suffix) * seconds
|
|
234
|
-
return None
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def _infer_gap_table_granularity_seconds(table: str) -> int | None:
|
|
238
|
-
if (match := _RATE_VIEW_NAME_RE.fullmatch(table)) is None:
|
|
239
|
-
return None
|
|
240
|
-
return _timeframe_interval_seconds(int(match.group("timeframe")))
|
|
241
|
-
|
|
242
|
-
|
|
243
213
|
def _default_gap_tables(conn: sqlite3.Connection) -> list[str]:
|
|
244
214
|
rows = conn.execute(
|
|
245
215
|
"SELECT name FROM sqlite_master"
|
|
@@ -364,10 +334,10 @@ def _callback( # pyright: ignore[reportUnusedFunction]
|
|
|
364
334
|
raise typer.BadParameter(str(exc)) from exc
|
|
365
335
|
try:
|
|
366
336
|
config = build_config(
|
|
367
|
-
path=
|
|
368
|
-
login=
|
|
369
|
-
password=
|
|
370
|
-
server=
|
|
337
|
+
path=path,
|
|
338
|
+
login=login,
|
|
339
|
+
password=password,
|
|
340
|
+
server=server,
|
|
371
341
|
timeout=timeout,
|
|
372
342
|
allow_whole_dollar_env=True,
|
|
373
343
|
)
|
|
@@ -988,7 +958,7 @@ def history_gaps(
|
|
|
988
958
|
frames: list[pd.DataFrame] = []
|
|
989
959
|
for table_name in tables:
|
|
990
960
|
interval_seconds = (
|
|
991
|
-
granularity_seconds or
|
|
961
|
+
granularity_seconds or infer_rate_table_granularity_seconds(table_name)
|
|
992
962
|
)
|
|
993
963
|
if interval_seconds is None:
|
|
994
964
|
msg = (
|
|
@@ -1141,7 +1111,6 @@ def grafana_schema(
|
|
|
1141
1111
|
import sqlite3 as _sqlite3 # noqa: PLC0415
|
|
1142
1112
|
|
|
1143
1113
|
from .grafana import ( # noqa: PLC0415
|
|
1144
|
-
create_snapshot_tables,
|
|
1145
1114
|
ensure_grafana_schema,
|
|
1146
1115
|
publish_grafana_copy,
|
|
1147
1116
|
)
|
|
@@ -1156,7 +1125,6 @@ def grafana_schema(
|
|
|
1156
1125
|
with _sqlite3.connect(export_ctx.output) as conn:
|
|
1157
1126
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
1158
1127
|
conn.execute("PRAGMA synchronous=NORMAL")
|
|
1159
|
-
create_snapshot_tables(conn)
|
|
1160
1128
|
ensure_grafana_schema(conn)
|
|
1161
1129
|
logger.info("Grafana schema applied to %s", export_ctx.output)
|
|
1162
1130
|
if publish_copy is not None:
|
|
@@ -9,7 +9,7 @@ import os
|
|
|
9
9
|
import sqlite3
|
|
10
10
|
import tempfile
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import cast
|
|
12
|
+
from typing import NamedTuple, cast
|
|
13
13
|
|
|
14
14
|
from .history import get_table_columns
|
|
15
15
|
|
|
@@ -158,113 +158,78 @@ def start_snapshot_run(conn: sqlite3.Connection, observed_at: int) -> int:
|
|
|
158
158
|
# ---------------------------------------------------------------------------
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
required = {"time", "symbol", "timeframe"}
|
|
164
|
-
if not required.issubset(cols):
|
|
165
|
-
logger.warning(
|
|
166
|
-
"Skipping grafana_rates: rates table missing columns %s",
|
|
167
|
-
sorted(required - cols),
|
|
168
|
-
)
|
|
169
|
-
return
|
|
170
|
-
time_expr = _time_col_expr("time")
|
|
171
|
-
others = _other_cols(cols, {"time"})
|
|
172
|
-
other_sql = ", ".join(f'"{c}"' for c in others)
|
|
173
|
-
_create_view_safe(
|
|
174
|
-
conn,
|
|
175
|
-
"grafana_rates",
|
|
176
|
-
f'SELECT {time_expr} AS "time", {other_sql} FROM "rates"', # noqa: S608
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def _build_grafana_ticks(conn: sqlite3.Connection) -> None:
|
|
181
|
-
cols = get_table_columns(conn, "ticks")
|
|
182
|
-
required = {"time", "symbol"}
|
|
183
|
-
if not required.issubset(cols):
|
|
184
|
-
logger.warning(
|
|
185
|
-
"Skipping grafana_ticks: ticks table missing columns %s",
|
|
186
|
-
sorted(required - cols),
|
|
187
|
-
)
|
|
188
|
-
return
|
|
189
|
-
time_expr = _time_col_expr("time")
|
|
190
|
-
others = _other_cols(cols, {"time"})
|
|
191
|
-
other_sql = ", ".join(f'"{c}"' for c in others)
|
|
192
|
-
_create_view_safe(
|
|
193
|
-
conn,
|
|
194
|
-
"grafana_ticks",
|
|
195
|
-
f'SELECT {time_expr} AS "time", {other_sql} FROM "ticks"', # noqa: S608
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def _build_grafana_history_deals(conn: sqlite3.Connection) -> None:
|
|
200
|
-
cols = get_table_columns(conn, "history_deals")
|
|
201
|
-
if "time" not in cols:
|
|
202
|
-
logger.warning("Skipping grafana_history_deals: history_deals.time is missing")
|
|
203
|
-
return
|
|
204
|
-
time_expr = _time_col_expr("time")
|
|
205
|
-
others = _other_cols(cols, {"time"})
|
|
206
|
-
other_sql = ", ".join(f'"{c}"' for c in others)
|
|
207
|
-
_create_view_safe(
|
|
208
|
-
conn,
|
|
209
|
-
"grafana_history_deals",
|
|
210
|
-
f'SELECT {time_expr} AS "time", {other_sql} FROM "history_deals"', # noqa: S608
|
|
211
|
-
)
|
|
161
|
+
class _PassthroughViewSpec(NamedTuple):
|
|
162
|
+
"""Declarative spec for a view that re-exposes one table with a time column.
|
|
212
163
|
|
|
164
|
+
``exclude_time_col`` controls whether the source time column is dropped
|
|
165
|
+
from the passthrough column list (``grafana_history_orders`` keeps its
|
|
166
|
+
original ``time_setup`` column alongside the derived ``time``).
|
|
167
|
+
"""
|
|
213
168
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
169
|
+
view: str
|
|
170
|
+
table: str
|
|
171
|
+
time_col: str
|
|
172
|
+
required: frozenset[str]
|
|
173
|
+
exclude_time_col: bool = True
|
|
174
|
+
where: str | None = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_PASSTHROUGH_VIEW_SPECS: tuple[_PassthroughViewSpec, ...] = (
|
|
178
|
+
_PassthroughViewSpec(
|
|
179
|
+
"grafana_rates", "rates", "time", frozenset({"time", "symbol", "timeframe"})
|
|
180
|
+
),
|
|
181
|
+
_PassthroughViewSpec(
|
|
182
|
+
"grafana_ticks", "ticks", "time", frozenset({"time", "symbol"})
|
|
183
|
+
),
|
|
184
|
+
_PassthroughViewSpec(
|
|
185
|
+
"grafana_history_deals", "history_deals", "time", frozenset({"time"})
|
|
186
|
+
),
|
|
187
|
+
_PassthroughViewSpec(
|
|
226
188
|
"grafana_history_orders",
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
required = {"time", "type"}
|
|
234
|
-
if not required.issubset(cols):
|
|
235
|
-
logger.warning(
|
|
236
|
-
"Skipping grafana_trade_deals: history_deals missing columns %s",
|
|
237
|
-
sorted(required - cols),
|
|
238
|
-
)
|
|
239
|
-
return
|
|
240
|
-
time_expr = _time_col_expr("time")
|
|
241
|
-
others = _other_cols(cols, {"time"})
|
|
242
|
-
other_sql = ", ".join(f'"{c}"' for c in others)
|
|
243
|
-
_create_view_safe(
|
|
244
|
-
conn,
|
|
189
|
+
"history_orders",
|
|
190
|
+
"time_setup",
|
|
191
|
+
frozenset({"time_setup"}),
|
|
192
|
+
exclude_time_col=False,
|
|
193
|
+
),
|
|
194
|
+
_PassthroughViewSpec(
|
|
245
195
|
"grafana_trade_deals",
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
196
|
+
"history_deals",
|
|
197
|
+
"time",
|
|
198
|
+
frozenset({"time", "type"}),
|
|
199
|
+
where=f'"type" IN {_TRADE_DEAL_TYPES_SQL}',
|
|
200
|
+
),
|
|
201
|
+
_PassthroughViewSpec(
|
|
202
|
+
"grafana_cash_events",
|
|
203
|
+
"history_deals",
|
|
204
|
+
"time",
|
|
205
|
+
frozenset({"time", "type"}),
|
|
206
|
+
where=f'"type" NOT IN {_TRADE_DEAL_TYPES_SQL}',
|
|
207
|
+
),
|
|
208
|
+
)
|
|
249
209
|
|
|
250
210
|
|
|
251
|
-
def
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
211
|
+
def _build_passthrough_view(
|
|
212
|
+
conn: sqlite3.Connection,
|
|
213
|
+
spec: _PassthroughViewSpec,
|
|
214
|
+
) -> None:
|
|
215
|
+
cols = get_table_columns(conn, spec.table)
|
|
216
|
+
if not spec.required.issubset(cols):
|
|
255
217
|
logger.warning(
|
|
256
|
-
"Skipping
|
|
257
|
-
|
|
218
|
+
"Skipping %s: %s table missing columns %s",
|
|
219
|
+
spec.view,
|
|
220
|
+
spec.table,
|
|
221
|
+
sorted(spec.required - cols),
|
|
258
222
|
)
|
|
259
223
|
return
|
|
260
|
-
time_expr = _time_col_expr(
|
|
261
|
-
others = _other_cols(cols, {
|
|
224
|
+
time_expr = _time_col_expr(spec.time_col)
|
|
225
|
+
others = _other_cols(cols, {spec.time_col} if spec.exclude_time_col else set())
|
|
262
226
|
other_sql = ", ".join(f'"{c}"' for c in others)
|
|
227
|
+
where_sql = f" WHERE {spec.where}" if spec.where else ""
|
|
263
228
|
_create_view_safe(
|
|
264
229
|
conn,
|
|
265
|
-
|
|
230
|
+
spec.view,
|
|
266
231
|
f'SELECT {time_expr} AS "time", {other_sql}' # noqa: S608
|
|
267
|
-
f' FROM "
|
|
232
|
+
f' FROM "{spec.table}"{where_sql}',
|
|
268
233
|
)
|
|
269
234
|
|
|
270
235
|
|
|
@@ -374,20 +339,12 @@ def _build_snapshot_view(
|
|
|
374
339
|
logger.warning("Skipping %s: snapshot_runs missing required columns", view_name)
|
|
375
340
|
|
|
376
341
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def _build_grafana_order_snapshots(conn: sqlite3.Connection) -> None:
|
|
386
|
-
_build_snapshot_view(conn, "grafana_order_snapshots", "order_snapshots")
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def _build_grafana_terminal_snapshots(conn: sqlite3.Connection) -> None:
|
|
390
|
-
_build_snapshot_view(conn, "grafana_terminal_snapshots", "terminal_snapshots")
|
|
342
|
+
_SNAPSHOT_VIEW_TABLES: tuple[tuple[str, str], ...] = (
|
|
343
|
+
("grafana_account_snapshots", "account_snapshots"),
|
|
344
|
+
("grafana_position_snapshots", "position_snapshots"),
|
|
345
|
+
("grafana_order_snapshots", "order_snapshots"),
|
|
346
|
+
("grafana_terminal_snapshots", "terminal_snapshots"),
|
|
347
|
+
)
|
|
391
348
|
|
|
392
349
|
|
|
393
350
|
# ---------------------------------------------------------------------------
|
|
@@ -404,19 +361,13 @@ def create_grafana_views(conn: sqlite3.Connection) -> None:
|
|
|
404
361
|
"""
|
|
405
362
|
for name in _GRAFANA_VIEW_NAMES:
|
|
406
363
|
conn.execute(f'DROP VIEW IF EXISTS "{name}"')
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
_build_grafana_history_deals(conn)
|
|
410
|
-
_build_grafana_history_orders(conn)
|
|
411
|
-
_build_grafana_trade_deals(conn)
|
|
412
|
-
_build_grafana_cash_events(conn)
|
|
364
|
+
for spec in _PASSTHROUGH_VIEW_SPECS:
|
|
365
|
+
_build_passthrough_view(conn, spec)
|
|
413
366
|
_build_grafana_realized_pnl(conn)
|
|
414
367
|
_build_grafana_symbol_pnl(conn)
|
|
415
368
|
_build_grafana_trade_stats(conn)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
_build_grafana_order_snapshots(conn)
|
|
419
|
-
_build_grafana_terminal_snapshots(conn)
|
|
369
|
+
for view_name, table_name in _SNAPSHOT_VIEW_TABLES:
|
|
370
|
+
_build_snapshot_view(conn, view_name, table_name)
|
|
420
371
|
|
|
421
372
|
|
|
422
373
|
def create_grafana_indexes(conn: sqlite3.Connection) -> None:
|
|
@@ -17,11 +17,7 @@ import pandas as pd
|
|
|
17
17
|
from pdmt5 import Mt5Config, Mt5RuntimeError
|
|
18
18
|
from pdmt5.constants import get_timeframe_name as _get_timeframe_name
|
|
19
19
|
|
|
20
|
-
from .client import
|
|
21
|
-
MT5Client,
|
|
22
|
-
_connected_client, # pyright: ignore[reportPrivateUsage]
|
|
23
|
-
build_config,
|
|
24
|
-
)
|
|
20
|
+
from .client import mt5_session
|
|
25
21
|
from .exceptions import Mt5ConnectionError
|
|
26
22
|
from .schemas import DEDUP_KEYS, REQUIRED_COLUMNS, DataKind, ensure_utc_columns
|
|
27
23
|
from .telemetry import get_metrics
|
|
@@ -158,6 +154,40 @@ def resolve_granularity_name(timeframe: int) -> str:
|
|
|
158
154
|
return name.removeprefix("TIMEFRAME_")
|
|
159
155
|
|
|
160
156
|
|
|
157
|
+
def timeframe_interval_seconds(timeframe: int) -> int | None:
|
|
158
|
+
"""Return the bar interval in seconds for a timeframe integer.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Interval seconds when the timeframe maps to a known granularity name
|
|
162
|
+
(for example ``M1`` -> ``60``), otherwise ``None``.
|
|
163
|
+
"""
|
|
164
|
+
granularity = resolve_granularity_name(timeframe)
|
|
165
|
+
units = {
|
|
166
|
+
"M": 60,
|
|
167
|
+
"H": 3600,
|
|
168
|
+
"D": 86400,
|
|
169
|
+
"W": 604800,
|
|
170
|
+
}
|
|
171
|
+
for prefix, seconds in units.items():
|
|
172
|
+
suffix = granularity.removeprefix(prefix)
|
|
173
|
+
if granularity.startswith(prefix) and suffix.isdigit():
|
|
174
|
+
return int(suffix) * seconds
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def infer_rate_table_granularity_seconds(table: str) -> int | None:
|
|
179
|
+
"""Infer the bar interval in seconds from a managed rate table/view name.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Interval seconds when ``table`` matches the managed
|
|
183
|
+
``rate_<symbol>__[<granularity>_]<timeframe>`` naming convention and
|
|
184
|
+
the timeframe is known, otherwise ``None``.
|
|
185
|
+
"""
|
|
186
|
+
if (match := _RATE_VIEW_NAME_RE.fullmatch(table)) is None:
|
|
187
|
+
return None
|
|
188
|
+
return timeframe_interval_seconds(int(match.group("timeframe")))
|
|
189
|
+
|
|
190
|
+
|
|
161
191
|
def drop_forming_rate_bar(df_rate: pd.DataFrame) -> pd.DataFrame:
|
|
162
192
|
"""Return closed bars from chronologically ordered MT5 rate data.
|
|
163
193
|
|
|
@@ -1719,9 +1749,17 @@ def _stream_symbol_frames(
|
|
|
1719
1749
|
"""
|
|
1720
1750
|
table_exists = False
|
|
1721
1751
|
for sym in symbols:
|
|
1752
|
+
frame = fetch_frame(sym)
|
|
1753
|
+
if len(frame.columns) == 0:
|
|
1754
|
+
logger.warning(
|
|
1755
|
+
"Skipping %s for symbol=%s: dataset returned no columns",
|
|
1756
|
+
dataset.table_name,
|
|
1757
|
+
sym,
|
|
1758
|
+
)
|
|
1759
|
+
continue
|
|
1722
1760
|
table_exists = write_streamed_frame(
|
|
1723
1761
|
conn,
|
|
1724
|
-
|
|
1762
|
+
frame,
|
|
1725
1763
|
dataset,
|
|
1726
1764
|
table_exists,
|
|
1727
1765
|
if_exists,
|
|
@@ -2597,11 +2635,7 @@ def update_history_with_config( # noqa: PLR0913
|
|
|
2597
2635
|
)
|
|
2598
2636
|
if request is None:
|
|
2599
2637
|
return
|
|
2600
|
-
|
|
2601
|
-
with _connected_client(mt5_config) as raw_client:
|
|
2602
|
-
client = MT5Client._from_connected_client( # pyright: ignore[reportPrivateUsage] # noqa: SLF001
|
|
2603
|
-
raw_client
|
|
2604
|
-
)
|
|
2638
|
+
with mt5_session(config) as client:
|
|
2605
2639
|
update_history(
|
|
2606
2640
|
client=client,
|
|
2607
2641
|
output=output,
|
|
@@ -2793,15 +2827,11 @@ def collect_history(
|
|
|
2793
2827
|
selected = resolve_history_datasets(datasets)
|
|
2794
2828
|
tf = parse_timeframe(timeframe)
|
|
2795
2829
|
tick_flags = parse_tick_flags(flags)
|
|
2796
|
-
mt5_config = config or build_config()
|
|
2797
2830
|
with (
|
|
2798
|
-
|
|
2831
|
+
mt5_session(config) as client,
|
|
2799
2832
|
closing(sqlite3.connect(output)) as conn,
|
|
2800
2833
|
conn,
|
|
2801
2834
|
):
|
|
2802
|
-
client = MT5Client._from_connected_client( # pyright: ignore[reportPrivateUsage] # noqa: SLF001
|
|
2803
|
-
raw_client
|
|
2804
|
-
)
|
|
2805
2835
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
2806
2836
|
conn.execute("PRAGMA synchronous=NORMAL")
|
|
2807
2837
|
written_tables, written_columns = write_collected_datasets(
|
|
@@ -10,8 +10,6 @@ from __future__ import annotations
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
from pydantic import SecretStr
|
|
14
|
-
|
|
15
13
|
from .client import (
|
|
16
14
|
MT5Client,
|
|
17
15
|
_require_positive, # pyright: ignore[reportPrivateUsage]
|
|
@@ -73,7 +71,7 @@ __all__ = [
|
|
|
73
71
|
|
|
74
72
|
|
|
75
73
|
def _make_client(*, config: Mt5Config | None = None) -> MT5Client:
|
|
76
|
-
return MT5Client(config=config)
|
|
74
|
+
return MT5Client(config=config)
|
|
77
75
|
|
|
78
76
|
|
|
79
77
|
def copy_rates_from(
|
|
@@ -348,9 +346,14 @@ def _build_account_config(
|
|
|
348
346
|
login = _coerce_login(account.login)
|
|
349
347
|
if login is None and base_config is not None:
|
|
350
348
|
login = base_config.login
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
349
|
+
raw_base_password = base_config.password if base_config else None
|
|
350
|
+
# pdmt5 stores the password as ``str | SecretStr | None``; unwrap the
|
|
351
|
+
# secret form without importing pydantic directly.
|
|
352
|
+
base_password = (
|
|
353
|
+
raw_base_password
|
|
354
|
+
if raw_base_password is None or isinstance(raw_base_password, str)
|
|
355
|
+
else raw_base_password.get_secret_value()
|
|
356
|
+
)
|
|
354
357
|
return build_config(
|
|
355
358
|
path=account.path or (base_config.path if base_config else None),
|
|
356
359
|
login=login,
|
|
@@ -15,11 +15,7 @@ from datetime import UTC, datetime
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import TYPE_CHECKING, cast
|
|
17
17
|
|
|
18
|
-
from .client import
|
|
19
|
-
MT5Client,
|
|
20
|
-
_connected_client, # pyright: ignore[reportPrivateUsage]
|
|
21
|
-
build_config,
|
|
22
|
-
)
|
|
18
|
+
from .client import mt5_session
|
|
23
19
|
from .grafana import (
|
|
24
20
|
create_snapshot_tables,
|
|
25
21
|
ensure_grafana_schema,
|
|
@@ -241,11 +237,7 @@ def update_observability_with_config(
|
|
|
241
237
|
include_terminal: Snapshot terminal info.
|
|
242
238
|
with_grafana_schema: Ensure Grafana views and indexes exist.
|
|
243
239
|
"""
|
|
244
|
-
|
|
245
|
-
with _connected_client(mt5_config) as raw_client:
|
|
246
|
-
client = MT5Client._from_connected_client( # pyright: ignore[reportPrivateUsage] # noqa: SLF001
|
|
247
|
-
raw_client
|
|
248
|
-
)
|
|
240
|
+
with mt5_session(config) as client:
|
|
249
241
|
update_observability(
|
|
250
242
|
client=client,
|
|
251
243
|
output=output,
|