mt5cli 1.7.0__tar.gz → 1.7.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.
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.agents/skills/local-qa/scripts/qa.sh +2 -2
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/dependabot.yml +11 -3
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/renovate.json +1 -1
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/workflows/ci.yml +0 -13
- mt5cli-1.7.1/.github/workflows/codeql.yml +42 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/workflows/release.yml +2 -2
- {mt5cli-1.7.0 → mt5cli-1.7.1}/PKG-INFO +4 -4
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/__init__.py +2 -2
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/analytics.py +12 -6
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/cli.py +3 -3
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/client.py +7 -7
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/history.py +31 -19
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/lifecycle.py +1 -1
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/rates.py +6 -3
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/schemas.py +13 -9
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/sdk.py +1 -1
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/telemetry.py +6 -6
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/trading.py +123 -64
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/utils.py +2 -2
- mt5cli-1.7.1/pyproject.toml +142 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/conftest.py +4 -4
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/packaging/test_metadata.py +1 -1
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_analytics.py +2 -2
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_cli.py +16 -13
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_client.py +1 -1
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_history.py +52 -5
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_marketdata.py +1 -1
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_observability.py +6 -5
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_trading.py +1 -1
- mt5cli-1.7.1/uv.lock +1570 -0
- mt5cli-1.7.0/pyproject.toml +0 -202
- mt5cli-1.7.0/uv.lock +0 -1504
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.claude/agents/codex.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.claude/settings.json +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/CODEOWNERS +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/FUNDING.yml +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/workflows/claude.yml +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/.gitignore +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/LICENSE +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/README.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/analytics.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/cli.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/client.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/converters.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/exceptions.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/history.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/index.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/marketdata.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/observability.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/public-contract.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/schemas.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/telemetry.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/trading.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/utils.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/index.md +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mkdocs.yml +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/__main__.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/contract.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/converters.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/exceptions.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/marketdata.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/observability.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/retry.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/architecture/__init__.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/architecture/test_unit_module_correspondence.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/cli/test_cli.bats +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/contracts/__init__.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/contracts/test_public_api.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/docs/__init__.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/docs/test_public_contract.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/docs/test_rate_storage_contract.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/integration/__init__.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/integration/test_history_capture_persist.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/__init__.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/conftest.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_contract.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_converters.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_exceptions.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_lifecycle.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_rates.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_retry.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_schemas.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_sdk.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_telemetry.py +0 -0
- {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_utils.py +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
set -euox pipefail
|
|
4
4
|
cd "$(git rev-parse --show-toplevel)"
|
|
5
5
|
|
|
6
|
-
COOLDOWN_DAYS=
|
|
6
|
+
COOLDOWN_DAYS=1
|
|
7
7
|
export UV_EXCLUDE_NEWER="${COOLDOWN_DAYS} days"
|
|
8
8
|
export NPM_CONFIG_MIN_RELEASE_AGE="${COOLDOWN_DAYS}"
|
|
9
9
|
export PNPM_CONFIG_MINIMUM_RELEASE_AGE=$((COOLDOWN_DAYS * 24 * 60))
|
|
@@ -16,7 +16,7 @@ uv run pyright .
|
|
|
16
16
|
uv run pytest
|
|
17
17
|
|
|
18
18
|
# Markdown and JSON
|
|
19
|
-
|
|
19
|
+
pnpm dlx oxfmt --write './**/*.{md,json}'
|
|
20
20
|
|
|
21
21
|
# YAML
|
|
22
22
|
git ls-files -z -- '*.yml' \
|
|
@@ -6,12 +6,20 @@ updates:
|
|
|
6
6
|
schedule:
|
|
7
7
|
interval: daily
|
|
8
8
|
cooldown:
|
|
9
|
-
default-days:
|
|
9
|
+
default-days: 1
|
|
10
10
|
open-pull-requests-limit: 10
|
|
11
|
-
|
|
11
|
+
groups:
|
|
12
|
+
github-actions-dependencies:
|
|
13
|
+
patterns:
|
|
14
|
+
- '*'
|
|
15
|
+
- package-ecosystem: uv
|
|
12
16
|
directory: /
|
|
13
17
|
schedule:
|
|
14
18
|
interval: daily
|
|
15
19
|
cooldown:
|
|
16
|
-
default-days:
|
|
20
|
+
default-days: 1
|
|
17
21
|
open-pull-requests-limit: 10
|
|
22
|
+
groups:
|
|
23
|
+
uv-dependencies:
|
|
24
|
+
patterns:
|
|
25
|
+
- '*'
|
|
@@ -73,19 +73,6 @@ jobs:
|
|
|
73
73
|
runs-on: ubuntu-slim
|
|
74
74
|
secrets:
|
|
75
75
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
76
|
-
github-codeql-analysis:
|
|
77
|
-
if: >
|
|
78
|
-
github.event_name == 'push'
|
|
79
|
-
|| github.event_name == 'pull_request'
|
|
80
|
-
|| (github.event_name == 'workflow_dispatch' && inputs.workflow == 'lint-and-test')
|
|
81
|
-
permissions:
|
|
82
|
-
contents: read
|
|
83
|
-
security-events: write
|
|
84
|
-
actions: read
|
|
85
|
-
uses: dceoy/gha-for-devops/.github/workflows/github-codeql-analysis.yml@main # zizmor: ignore[unpinned-uses]
|
|
86
|
-
with:
|
|
87
|
-
language: >
|
|
88
|
-
["python"]
|
|
89
76
|
dependabot-auto-merge:
|
|
90
77
|
if: >
|
|
91
78
|
github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: CodeQL
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- '.github/workflows/**'
|
|
9
|
+
- '.github/actions/**'
|
|
10
|
+
- '**/action.yml'
|
|
11
|
+
- '**/action.yaml'
|
|
12
|
+
- '**/*.py'
|
|
13
|
+
- '**/*.pyi'
|
|
14
|
+
- '**/pyproject.toml'
|
|
15
|
+
- '**/uv.lock'
|
|
16
|
+
- '**/requirements*.txt'
|
|
17
|
+
pull_request:
|
|
18
|
+
branches:
|
|
19
|
+
- main
|
|
20
|
+
paths:
|
|
21
|
+
- '.github/workflows/**'
|
|
22
|
+
- '.github/actions/**'
|
|
23
|
+
- '**/action.yml'
|
|
24
|
+
- '**/action.yaml'
|
|
25
|
+
- '**/*.py'
|
|
26
|
+
- '**/*.pyi'
|
|
27
|
+
- '**/pyproject.toml'
|
|
28
|
+
- '**/uv.lock'
|
|
29
|
+
- '**/requirements*.txt'
|
|
30
|
+
workflow_dispatch:
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
jobs:
|
|
34
|
+
github-codeql-analysis:
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
security-events: write
|
|
38
|
+
actions: read
|
|
39
|
+
uses: dceoy/gha-for-devops/.github/workflows/github-codeql-analysis.yml@main # zizmor: ignore[unpinned-uses]
|
|
40
|
+
with:
|
|
41
|
+
language: >
|
|
42
|
+
["actions","python"]
|
|
@@ -34,11 +34,11 @@ jobs:
|
|
|
34
34
|
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
35
35
|
steps:
|
|
36
36
|
- name: Download all the dists
|
|
37
|
-
uses: actions/download-artifact@
|
|
37
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
38
38
|
with:
|
|
39
39
|
name: ${{ needs.build-and-release.outputs.distribution-artifact-name }}
|
|
40
40
|
path: dist/
|
|
41
41
|
- name: Publish distribution 📦 to PyPI
|
|
42
|
-
uses: pypa/gh-action-pypi-publish@
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
43
43
|
with:
|
|
44
44
|
verbose: true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.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>
|
|
@@ -20,9 +20,9 @@ Requires-Dist: pandas>=2.2.2
|
|
|
20
20
|
Requires-Dist: pdmt5>=1.2.0
|
|
21
21
|
Requires-Dist: typer>=0.26.0
|
|
22
22
|
Provides-Extra: otel
|
|
23
|
-
Requires-Dist: opentelemetry-api; extra == 'otel'
|
|
24
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-http; extra == 'otel'
|
|
25
|
-
Requires-Dist: opentelemetry-sdk; extra == 'otel'
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.23.0; extra == 'otel'
|
|
24
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.23.0; extra == 'otel'
|
|
25
|
+
Requires-Dist: opentelemetry-sdk>=1.23.0; extra == 'otel'
|
|
26
26
|
Provides-Extra: parquet
|
|
27
27
|
Requires-Dist: pyarrow>=19.0.0; extra == 'parquet'
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
@@ -6,7 +6,7 @@ the stable SDK contract, CLI surface, internal modules, and out-of-scope
|
|
|
6
6
|
strategy responsibilities.
|
|
7
7
|
"""
|
|
8
8
|
# pyright: reportUnusedImport=false, reportUnsupportedDunderAll=false
|
|
9
|
-
# ruff:
|
|
9
|
+
# ruff: file-ignore[undefined-local-with-import-star]
|
|
10
10
|
|
|
11
11
|
from importlib.metadata import version as _package_version
|
|
12
12
|
|
|
@@ -15,4 +15,4 @@ from .sdk import STABLE_SDK_EXPORTS
|
|
|
15
15
|
from .sdk import __all__ as _sdk_exports
|
|
16
16
|
|
|
17
17
|
__version__ = _package_version(__package__) if __package__ else None
|
|
18
|
-
__all__ = [*_sdk_exports, "STABLE_SDK_EXPORTS"] #
|
|
18
|
+
__all__ = [*_sdk_exports, "STABLE_SDK_EXPORTS"] # ruff: ignore[invalid-all-object]
|
|
@@ -18,6 +18,7 @@ from .history import (
|
|
|
18
18
|
create_deal_portions_view,
|
|
19
19
|
get_table_columns,
|
|
20
20
|
open_existing_sqlite_database,
|
|
21
|
+
quote_sqlite_identifier,
|
|
21
22
|
)
|
|
22
23
|
from .utils import export_dataframe
|
|
23
24
|
|
|
@@ -98,8 +99,13 @@ def _create_view(
|
|
|
98
99
|
*,
|
|
99
100
|
temporary: bool,
|
|
100
101
|
) -> None:
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
name = quote_sqlite_identifier(name)
|
|
103
|
+
# SQLite cannot bind identifiers or view bodies. The SELECT is built only
|
|
104
|
+
# from module-owned SQL templates; the view name is escaped above.
|
|
105
|
+
# nosemgrep: formatted-sql-query, sqlalchemy-execute-raw-query
|
|
106
|
+
conn.execute(f"DROP VIEW IF EXISTS {'temp.' if temporary else ''}{name}")
|
|
107
|
+
# nosemgrep: formatted-sql-query, sqlalchemy-execute-raw-query
|
|
108
|
+
conn.execute(f"CREATE {'TEMP ' if temporary else ''}VIEW {name} AS {select_sql}")
|
|
103
109
|
|
|
104
110
|
|
|
105
111
|
_CLOSE_DATE_SQL = (
|
|
@@ -107,7 +113,7 @@ _CLOSE_DATE_SQL = (
|
|
|
107
113
|
" THEN date({col}, 'unixepoch') ELSE date({col}) END"
|
|
108
114
|
)
|
|
109
115
|
_TRADES_SQL = (
|
|
110
|
-
"SELECT position_id, symbol, leg_index, magic, magic_count, side, open_time," #
|
|
116
|
+
"SELECT position_id, symbol, leg_index, magic, magic_count, side, open_time," # ruff: ignore[hardcoded-sql-expression]
|
|
111
117
|
" close_time,"
|
|
112
118
|
f" {_CLOSE_DATE_SQL.format(col='close_time')} AS close_date,"
|
|
113
119
|
" CASE"
|
|
@@ -155,7 +161,7 @@ _TRADES_SQL = (
|
|
|
155
161
|
# magic, including portions of legs that are still open or whose opening is not in
|
|
156
162
|
# the captured history (trades/strategy stats keep completed legs only).
|
|
157
163
|
_REALIZED_EVENTS_SQL = (
|
|
158
|
-
"SELECT time," #
|
|
164
|
+
"SELECT time," # ruff: ignore[hardcoded-sql-expression]
|
|
159
165
|
f" {_CLOSE_DATE_SQL.format(col='time')} AS date,"
|
|
160
166
|
" ticket, position_id, symbol, magic, leg AS leg_index, role,"
|
|
161
167
|
" profit, commission, swap, fee,"
|
|
@@ -186,7 +192,7 @@ _DAILY_PNL_SQL = (
|
|
|
186
192
|
" GROUP BY date, symbol, magic"
|
|
187
193
|
)
|
|
188
194
|
_STRATEGY_STATS_SQL = (
|
|
189
|
-
f"SELECT symbol, magic, {_METRICS_SQL}," #
|
|
195
|
+
f"SELECT symbol, magic, {_METRICS_SQL}," # ruff: ignore[hardcoded-sql-expression]
|
|
190
196
|
" AVG(holding_seconds) AS avg_holding_seconds,"
|
|
191
197
|
" MIN(open_time) AS first_open_time, MAX(close_time) AS last_close_time"
|
|
192
198
|
" FROM analytics_trades GROUP BY symbol, magic"
|
|
@@ -282,7 +288,7 @@ def _collect_dataset_frames(conn: sqlite3.Connection) -> dict[str, pd.DataFrame]
|
|
|
282
288
|
for dataset, view in _VIEW_DATASETS:
|
|
283
289
|
frames[dataset] = _apply_schema(
|
|
284
290
|
pd.read_sql_query( # pyright: ignore[reportUnknownMemberType]
|
|
285
|
-
f'SELECT * FROM "{view}"', #
|
|
291
|
+
f'SELECT * FROM "{view}"', # ruff: ignore[hardcoded-sql-expression]
|
|
286
292
|
conn,
|
|
287
293
|
),
|
|
288
294
|
)
|
|
@@ -6,8 +6,8 @@ import json
|
|
|
6
6
|
import logging
|
|
7
7
|
from contextlib import closing
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
-
from datetime import datetime #
|
|
10
|
-
from pathlib import Path #
|
|
9
|
+
from datetime import datetime # ruff: ignore[typing-only-standard-library-import]
|
|
10
|
+
from pathlib import Path # ruff: ignore[typing-only-standard-library-import]
|
|
11
11
|
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
12
12
|
|
|
13
13
|
import pandas as pd
|
|
@@ -1003,7 +1003,7 @@ def publish_dashboard_command(
|
|
|
1003
1003
|
typer.BadParameter: If the source database is invalid, the manifest path
|
|
1004
1004
|
is not JSON, or no dataset can be built.
|
|
1005
1005
|
"""
|
|
1006
|
-
from .analytics import publish_dashboard #
|
|
1006
|
+
from .analytics import publish_dashboard # ruff: ignore[import-outside-top-level]
|
|
1007
1007
|
|
|
1008
1008
|
export_ctx = _get_export_context(ctx)
|
|
1009
1009
|
if export_ctx.output_format != "json":
|
|
@@ -26,7 +26,7 @@ from .utils import coerce_login as _coerce_login
|
|
|
26
26
|
from .utils import parse_tick_flags, parse_timeframe
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
|
-
from collections.abc import Callable, Collection,
|
|
29
|
+
from collections.abc import Callable, Collection, Generator, Sequence
|
|
30
30
|
|
|
31
31
|
T = TypeVar("T")
|
|
32
32
|
|
|
@@ -373,7 +373,7 @@ def _connected_client(
|
|
|
373
373
|
config: Mt5Config,
|
|
374
374
|
*,
|
|
375
375
|
retry_count: int | None = None,
|
|
376
|
-
) ->
|
|
376
|
+
) -> Generator[Mt5DataClient, None, None]:
|
|
377
377
|
"""Initialize MT5, yield a connected client, and always shut down.
|
|
378
378
|
|
|
379
379
|
Private lifecycle primitive owned by this module. Other mt5cli modules
|
|
@@ -479,7 +479,7 @@ class _BaseMT5Client:
|
|
|
479
479
|
|
|
480
480
|
@property
|
|
481
481
|
def config(self) -> Mt5Config:
|
|
482
|
-
"""
|
|
482
|
+
"""Underlying MT5 configuration."""
|
|
483
483
|
return self._config
|
|
484
484
|
|
|
485
485
|
def __enter__(self) -> Self:
|
|
@@ -964,8 +964,8 @@ class MT5Client(_BaseMT5Client):
|
|
|
964
964
|
return cls._from_connected_client(client)
|
|
965
965
|
|
|
966
966
|
@property
|
|
967
|
-
def mt5(self) -> Any: #
|
|
968
|
-
"""
|
|
967
|
+
def mt5(self) -> Any: # ruff: ignore[any-type]
|
|
968
|
+
"""MT5 constants required by operational helpers.
|
|
969
969
|
|
|
970
970
|
This intentionally exposes constants only, never the pdmt5 client.
|
|
971
971
|
"""
|
|
@@ -1042,7 +1042,7 @@ class MT5Client(_BaseMT5Client):
|
|
|
1042
1042
|
@contextmanager
|
|
1043
1043
|
def mt5_session(
|
|
1044
1044
|
config: Mt5Config | None = None, *, client: MT5Client | None = None
|
|
1045
|
-
) ->
|
|
1045
|
+
) -> Generator[MT5Client, None, None]:
|
|
1046
1046
|
"""Open an MT5 terminal session and yield a connected :class:`MT5Client`.
|
|
1047
1047
|
|
|
1048
1048
|
This is the sole public MT5 session factory. It is the only supported way
|
|
@@ -1063,6 +1063,6 @@ def mt5_session(
|
|
|
1063
1063
|
return
|
|
1064
1064
|
mt5_config = config or build_config()
|
|
1065
1065
|
with _connected_client(mt5_config) as raw_client:
|
|
1066
|
-
yield MT5Client._from_connected_client( # pyright: ignore[reportPrivateUsage] #
|
|
1066
|
+
yield MT5Client._from_connected_client( # pyright: ignore[reportPrivateUsage] # ruff: ignore[private-member-access]
|
|
1067
1067
|
raw_client
|
|
1068
1068
|
)
|
|
@@ -311,7 +311,7 @@ def _iter_rate_gap_groups(frame: pd.DataFrame) -> list[pd.DataFrame]:
|
|
|
311
311
|
]
|
|
312
312
|
|
|
313
313
|
|
|
314
|
-
def report_rate_gaps( #
|
|
314
|
+
def report_rate_gaps( # ruff: ignore[complex-structure, too-many-locals]
|
|
315
315
|
conn: sqlite3.Connection,
|
|
316
316
|
table: str,
|
|
317
317
|
*,
|
|
@@ -341,7 +341,7 @@ def report_rate_gaps( # noqa: C901, PLR0914
|
|
|
341
341
|
frame = cast(
|
|
342
342
|
"pd.DataFrame",
|
|
343
343
|
pd.read_sql_query( # type: ignore[reportUnknownMemberType]
|
|
344
|
-
f"SELECT * FROM {quoted_table} ORDER BY time ASC", #
|
|
344
|
+
f"SELECT * FROM {quoted_table} ORDER BY time ASC", # ruff: ignore[hardcoded-sql-expression]
|
|
345
345
|
conn,
|
|
346
346
|
),
|
|
347
347
|
)
|
|
@@ -475,7 +475,7 @@ def load_rate_data_from_connection(
|
|
|
475
475
|
frame = cast(
|
|
476
476
|
"pd.DataFrame",
|
|
477
477
|
pd.read_sql_query( # type: ignore[reportUnknownMemberType]
|
|
478
|
-
f"SELECT * FROM {quoted_table} ORDER BY time ASC", #
|
|
478
|
+
f"SELECT * FROM {quoted_table} ORDER BY time ASC", # ruff: ignore[hardcoded-sql-expression]
|
|
479
479
|
connection,
|
|
480
480
|
),
|
|
481
481
|
)
|
|
@@ -483,7 +483,7 @@ def load_rate_data_from_connection(
|
|
|
483
483
|
frame = cast(
|
|
484
484
|
"pd.DataFrame",
|
|
485
485
|
pd.read_sql_query( # type: ignore[reportUnknownMemberType]
|
|
486
|
-
f"SELECT * FROM {quoted_table} ORDER BY time DESC LIMIT ?", #
|
|
486
|
+
f"SELECT * FROM {quoted_table} ORDER BY time DESC LIMIT ?", # ruff: ignore[hardcoded-sql-expression]
|
|
487
487
|
connection,
|
|
488
488
|
params=(count,),
|
|
489
489
|
),
|
|
@@ -538,7 +538,7 @@ class RateTarget:
|
|
|
538
538
|
|
|
539
539
|
@property
|
|
540
540
|
def timeframe_int(self) -> int:
|
|
541
|
-
"""
|
|
541
|
+
"""Integer MT5 timeframe value."""
|
|
542
542
|
return cast("int", self.timeframe)
|
|
543
543
|
|
|
544
544
|
|
|
@@ -581,9 +581,11 @@ def build_rate_targets(
|
|
|
581
581
|
|
|
582
582
|
def get_table_columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
583
583
|
"""Return existing SQLite columns for a table."""
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
584
|
+
rows = conn.execute(
|
|
585
|
+
"SELECT name FROM pragma_table_info(?)",
|
|
586
|
+
(table,),
|
|
587
|
+
).fetchall()
|
|
588
|
+
return {str(row[0]) for row in rows}
|
|
587
589
|
|
|
588
590
|
|
|
589
591
|
def _parse_string_sqlite_timestamp(value: str) -> datetime | None:
|
|
@@ -700,11 +702,16 @@ def _load_latest_parseable_time(
|
|
|
700
702
|
) -> datetime | None:
|
|
701
703
|
quoted_table = quote_sqlite_identifier(table)
|
|
702
704
|
time_expr = _sqlite_normalized_time_expression("time")
|
|
703
|
-
query = f"SELECT time FROM {quoted_table} WHERE {time_expr} IS NOT NULL" #
|
|
705
|
+
query = f"SELECT time FROM {quoted_table} WHERE {time_expr} IS NOT NULL" # ruff: ignore[hardcoded-sql-expression]
|
|
704
706
|
if where_clause:
|
|
705
707
|
query += f" AND {where_clause}"
|
|
706
708
|
query += f" ORDER BY {time_expr} DESC, ROWID DESC LIMIT 1"
|
|
707
|
-
|
|
709
|
+
# Identifiers are quoted above; predicates are internal SQL fragments and
|
|
710
|
+
# all external filter values are bound through params.
|
|
711
|
+
row = conn.execute( # nosemgrep: sqlalchemy-execute-raw-query
|
|
712
|
+
query,
|
|
713
|
+
tuple(params),
|
|
714
|
+
).fetchone()
|
|
708
715
|
return parse_sqlite_timestamp(row[0] if row else None)
|
|
709
716
|
|
|
710
717
|
|
|
@@ -1010,14 +1017,14 @@ def drop_duplicates_in_table(
|
|
|
1010
1017
|
)
|
|
1011
1018
|
if scope_where:
|
|
1012
1019
|
delete_sql = (
|
|
1013
|
-
f"DELETE FROM {table} WHERE {scope_where} AND ROWID NOT IN" #
|
|
1020
|
+
f"DELETE FROM {table} WHERE {scope_where} AND ROWID NOT IN" # ruff: ignore[hardcoded-sql-expression]
|
|
1014
1021
|
f" (SELECT {rowid_selector}(ROWID) FROM {table} WHERE {scope_where}"
|
|
1015
1022
|
f" GROUP BY {ids_csv})"
|
|
1016
1023
|
)
|
|
1017
1024
|
cursor.execute(delete_sql, prepared_scope_params + prepared_scope_params)
|
|
1018
1025
|
return
|
|
1019
1026
|
cursor.execute(
|
|
1020
|
-
f"DELETE FROM {table} WHERE ROWID NOT IN" #
|
|
1027
|
+
f"DELETE FROM {table} WHERE ROWID NOT IN" # ruff: ignore[hardcoded-sql-expression]
|
|
1021
1028
|
f" (SELECT {rowid_selector}(ROWID) FROM {table} GROUP BY {ids_csv})",
|
|
1022
1029
|
)
|
|
1023
1030
|
|
|
@@ -1232,7 +1239,7 @@ def create_cash_events_view(
|
|
|
1232
1239
|
return False
|
|
1233
1240
|
conn.execute("DROP VIEW IF EXISTS cash_events")
|
|
1234
1241
|
conn.execute(
|
|
1235
|
-
"CREATE VIEW cash_events AS" #
|
|
1242
|
+
"CREATE VIEW cash_events AS" # ruff: ignore[hardcoded-sql-expression]
|
|
1236
1243
|
f" SELECT * FROM history_deals WHERE type NOT IN {_TRADE_DEAL_TYPES_SQL}",
|
|
1237
1244
|
)
|
|
1238
1245
|
return True
|
|
@@ -1280,7 +1287,7 @@ def _deal_portions_select_sql(deal_columns: set[str]) -> str:
|
|
|
1280
1287
|
)
|
|
1281
1288
|
keys = "ticket, position_id, symbol, time, type, price, magic, position_start_known"
|
|
1282
1289
|
return (
|
|
1283
|
-
"WITH d AS (SELECT" #
|
|
1290
|
+
"WITH d AS (SELECT" # ruff: ignore[hardcoded-sql-expression]
|
|
1284
1291
|
f" {ticket} AS ticket, position_id, symbol, time, type, entry, volume,"
|
|
1285
1292
|
f" price, {magic} AS magic, COALESCE(profit, 0) AS profit, {costs},"
|
|
1286
1293
|
" CASE type WHEN 0 THEN volume ELSE -volume END AS delta"
|
|
@@ -1322,7 +1329,12 @@ def _create_history_view(
|
|
|
1322
1329
|
*,
|
|
1323
1330
|
temporary: bool,
|
|
1324
1331
|
) -> None:
|
|
1332
|
+
name = quote_sqlite_identifier(name)
|
|
1333
|
+
# SQLite cannot bind identifiers or view bodies. Callers supply internal
|
|
1334
|
+
# SELECT templates, and the view name is escaped above.
|
|
1335
|
+
# nosemgrep: formatted-sql-query, sqlalchemy-execute-raw-query
|
|
1325
1336
|
conn.execute(f"DROP VIEW IF EXISTS {'temp.' if temporary else ''}{name}")
|
|
1337
|
+
# nosemgrep: formatted-sql-query, sqlalchemy-execute-raw-query
|
|
1326
1338
|
conn.execute(f"CREATE {'TEMP ' if temporary else ''}VIEW {name} AS {select_sql}")
|
|
1327
1339
|
|
|
1328
1340
|
|
|
@@ -2355,7 +2367,7 @@ def _finalize_incremental_writes(
|
|
|
2355
2367
|
)
|
|
2356
2368
|
|
|
2357
2369
|
|
|
2358
|
-
def write_incremental_datasets( #
|
|
2370
|
+
def write_incremental_datasets( # ruff: ignore[too-many-arguments]
|
|
2359
2371
|
conn: sqlite3.Connection,
|
|
2360
2372
|
client: HistoryClient,
|
|
2361
2373
|
symbols: Sequence[str],
|
|
@@ -2634,7 +2646,7 @@ def _resolve_update_history_request(
|
|
|
2634
2646
|
)
|
|
2635
2647
|
|
|
2636
2648
|
|
|
2637
|
-
def update_history( #
|
|
2649
|
+
def update_history( # ruff: ignore[too-many-arguments]
|
|
2638
2650
|
*,
|
|
2639
2651
|
client: HistoryClient,
|
|
2640
2652
|
output: Path | str,
|
|
@@ -2716,7 +2728,7 @@ def update_history( # noqa: PLR0913
|
|
|
2716
2728
|
m.add_history_rows(conn.total_changes - before, dataset="history")
|
|
2717
2729
|
|
|
2718
2730
|
|
|
2719
|
-
def update_history_with_config( #
|
|
2731
|
+
def update_history_with_config( # ruff: ignore[too-many-arguments]
|
|
2720
2732
|
*,
|
|
2721
2733
|
output: Path | str,
|
|
2722
2734
|
symbols: Sequence[str],
|
|
@@ -2790,7 +2802,7 @@ def _open_history_cursor_reference(output_path: Path) -> sqlite3.Connection:
|
|
|
2790
2802
|
return sqlite3.connect(":memory:")
|
|
2791
2803
|
|
|
2792
2804
|
|
|
2793
|
-
def capture_history_datasets( #
|
|
2805
|
+
def capture_history_datasets( # ruff: ignore[too-many-arguments]
|
|
2794
2806
|
*,
|
|
2795
2807
|
client: HistoryClient,
|
|
2796
2808
|
output: Path | str,
|
|
@@ -2974,7 +2986,7 @@ class ThrottledHistoryUpdater:
|
|
|
2974
2986
|
|
|
2975
2987
|
@property
|
|
2976
2988
|
def last_update_monotonic(self) -> float | None:
|
|
2977
|
-
"""
|
|
2989
|
+
"""Monotonic timestamp of the last successful update."""
|
|
2978
2990
|
return self._last_update_monotonic
|
|
2979
2991
|
|
|
2980
2992
|
def should_update(self) -> bool:
|
|
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
|
|
14
14
|
from .client import MT5Client
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def _account_matches(account: Any, *, login: int, server: str | None) -> bool: #
|
|
17
|
+
def _account_matches(account: Any, *, login: int, server: str | None) -> bool: # ruff: ignore[any-type]
|
|
18
18
|
"""Return whether an MT5 account snapshot matches the requested account."""
|
|
19
19
|
if getattr(account, "login", None) != login:
|
|
20
20
|
return False
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""Canonical normalized-rate persistence and loading APIs."""
|
|
2
|
-
# ruff:
|
|
2
|
+
# ruff: file-ignore[too-many-arguments]
|
|
3
3
|
# pyright: reportPrivateUsage=false
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
@@ -100,6 +100,9 @@ def _table_contains_aware_timestamp_text(
|
|
|
100
100
|
time_columns = TIME_COLUMNS[kind] & _SQLITE_TEXT_TIME_COLUMNS & columns
|
|
101
101
|
for column in sorted(time_columns):
|
|
102
102
|
quoted_column = quote_sqlite_identifier(column)
|
|
103
|
+
# Only quoted table/column identifiers are interpolated; SQLite cannot
|
|
104
|
+
# bind identifiers, and every SQL value here is a fixed literal.
|
|
105
|
+
# nosemgrep: formatted-sql-query, sqlalchemy-execute-raw-query
|
|
103
106
|
row = conn.execute(
|
|
104
107
|
f"""
|
|
105
108
|
SELECT 1
|
|
@@ -112,7 +115,7 @@ def _table_contains_aware_timestamp_text(
|
|
|
112
115
|
OR instr(substr(trim({quoted_column}), 20), '-') > 0
|
|
113
116
|
)
|
|
114
117
|
LIMIT 1
|
|
115
|
-
""" #
|
|
118
|
+
""" # ruff: ignore[hardcoded-sql-expression]
|
|
116
119
|
).fetchone()
|
|
117
120
|
if row is not None:
|
|
118
121
|
return True
|
|
@@ -416,7 +419,7 @@ def load_rate_series_by_granularity(
|
|
|
416
419
|
Raises:
|
|
417
420
|
TypeError: If explicit table loading returns a single DataFrame.
|
|
418
421
|
"""
|
|
419
|
-
from .history import build_rate_targets #
|
|
422
|
+
from .history import build_rate_targets # ruff: ignore[import-outside-top-level]
|
|
420
423
|
|
|
421
424
|
targets = build_rate_targets(
|
|
422
425
|
symbols,
|
|
@@ -222,6 +222,13 @@ def _parse_mt5_time_value(value: object, *, unit: Literal["s", "ms"]) -> object:
|
|
|
222
222
|
)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
def _parse_mixed_mt5_time_value(value: object) -> pd.Timestamp:
|
|
226
|
+
return cast(
|
|
227
|
+
"pd.Timestamp",
|
|
228
|
+
pd.to_datetime(cast("Any", value), errors="coerce", format="mixed"),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
225
232
|
def _mt5_time_sort_key(series: pd.Series) -> pd.Series:
|
|
226
233
|
"""Build UTC sort keys without changing stored MT5 timestamp values.
|
|
227
234
|
|
|
@@ -251,19 +258,16 @@ def _coerce_mt5_time_column(series: pd.Series, column: str) -> pd.Series:
|
|
|
251
258
|
if pd.api.types.is_numeric_dtype(series):
|
|
252
259
|
return pd.to_datetime(series, unit=unit, errors="coerce")
|
|
253
260
|
if pd.api.types.is_object_dtype(series.dtype):
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
|
|
262
|
+
def parse_value(value: object) -> object:
|
|
263
|
+
return _parse_mt5_time_value(value, unit=unit)
|
|
264
|
+
|
|
265
|
+
parsed = series.map(parse_value)
|
|
257
266
|
return _normalize_parsed_mt5_times(parsed)
|
|
258
267
|
try:
|
|
259
268
|
parsed = pd.to_datetime(series, errors="coerce", format="mixed")
|
|
260
269
|
except ValueError:
|
|
261
|
-
parsed = series.map(
|
|
262
|
-
lambda value: cast(
|
|
263
|
-
"pd.Timestamp",
|
|
264
|
-
pd.to_datetime(value, errors="coerce", format="mixed"),
|
|
265
|
-
),
|
|
266
|
-
)
|
|
270
|
+
parsed = series.map(_parse_mixed_mt5_time_value)
|
|
267
271
|
return _normalize_parsed_mt5_times(parsed)
|
|
268
272
|
|
|
269
273
|
|
|
@@ -8,7 +8,7 @@ from contextlib import contextmanager
|
|
|
8
8
|
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
from collections.abc import
|
|
11
|
+
from collections.abc import Generator
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
@@ -181,10 +181,10 @@ class _Mt5Metrics:
|
|
|
181
181
|
(spec.key for spec in _INSTRUMENT_SPECS), _NOOP
|
|
182
182
|
)
|
|
183
183
|
|
|
184
|
-
def _instrument(self, key: str) -> Any: #
|
|
184
|
+
def _instrument(self, key: str) -> Any: # ruff: ignore[any-type]
|
|
185
185
|
return self._instruments[key]
|
|
186
186
|
|
|
187
|
-
def configure(self, meter: Any) -> None: #
|
|
187
|
+
def configure(self, meter: Any) -> None: # ruff: ignore[any-type]
|
|
188
188
|
"""Set up metric instruments from a meter object.
|
|
189
189
|
|
|
190
190
|
Args:
|
|
@@ -207,7 +207,7 @@ class _Mt5Metrics:
|
|
|
207
207
|
self,
|
|
208
208
|
*,
|
|
209
209
|
dataset: str,
|
|
210
|
-
) ->
|
|
210
|
+
) -> Generator[None, None, None]:
|
|
211
211
|
"""Context manager recording history update duration and failures.
|
|
212
212
|
|
|
213
213
|
Args:
|
|
@@ -236,7 +236,7 @@ class _Mt5Metrics:
|
|
|
236
236
|
self._instrument("history_rows").add(count, {"dataset": dataset})
|
|
237
237
|
|
|
238
238
|
@contextmanager
|
|
239
|
-
def record_snapshot_update(self) ->
|
|
239
|
+
def record_snapshot_update(self) -> Generator[None, None, None]:
|
|
240
240
|
"""Context manager recording snapshot update duration and failures.
|
|
241
241
|
|
|
242
242
|
Yields:
|
|
@@ -323,7 +323,7 @@ class _Mt5Metrics:
|
|
|
323
323
|
_metrics = _Mt5Metrics()
|
|
324
324
|
|
|
325
325
|
|
|
326
|
-
def configure_metrics(meter: Any) -> None: #
|
|
326
|
+
def configure_metrics(meter: Any) -> None: # ruff: ignore[any-type]
|
|
327
327
|
"""Configure MT5 metrics using the provided meter.
|
|
328
328
|
|
|
329
329
|
Args:
|