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.
Files changed (87) hide show
  1. {mt5cli-1.7.0 → mt5cli-1.7.1}/.agents/skills/local-qa/scripts/qa.sh +2 -2
  2. {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/dependabot.yml +11 -3
  3. {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/renovate.json +1 -1
  4. {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/workflows/ci.yml +0 -13
  5. mt5cli-1.7.1/.github/workflows/codeql.yml +42 -0
  6. {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/workflows/release.yml +2 -2
  7. {mt5cli-1.7.0 → mt5cli-1.7.1}/PKG-INFO +4 -4
  8. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/__init__.py +2 -2
  9. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/analytics.py +12 -6
  10. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/cli.py +3 -3
  11. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/client.py +7 -7
  12. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/history.py +31 -19
  13. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/lifecycle.py +1 -1
  14. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/rates.py +6 -3
  15. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/schemas.py +13 -9
  16. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/sdk.py +1 -1
  17. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/telemetry.py +6 -6
  18. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/trading.py +123 -64
  19. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/utils.py +2 -2
  20. mt5cli-1.7.1/pyproject.toml +142 -0
  21. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/conftest.py +4 -4
  22. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/packaging/test_metadata.py +1 -1
  23. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_analytics.py +2 -2
  24. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_cli.py +16 -13
  25. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_client.py +1 -1
  26. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_history.py +52 -5
  27. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_marketdata.py +1 -1
  28. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_observability.py +6 -5
  29. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_trading.py +1 -1
  30. mt5cli-1.7.1/uv.lock +1570 -0
  31. mt5cli-1.7.0/pyproject.toml +0 -202
  32. mt5cli-1.7.0/uv.lock +0 -1504
  33. {mt5cli-1.7.0 → mt5cli-1.7.1}/.agents/skills/local-qa/SKILL.md +0 -0
  34. {mt5cli-1.7.0 → mt5cli-1.7.1}/.agents/skills/mt5cli/SKILL.md +0 -0
  35. {mt5cli-1.7.0 → mt5cli-1.7.1}/.claude/agents/codex.md +0 -0
  36. {mt5cli-1.7.0 → mt5cli-1.7.1}/.claude/settings.json +0 -0
  37. {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/CODEOWNERS +0 -0
  38. {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/FUNDING.yml +0 -0
  39. {mt5cli-1.7.0 → mt5cli-1.7.1}/.github/workflows/claude.yml +0 -0
  40. {mt5cli-1.7.0 → mt5cli-1.7.1}/.gitignore +0 -0
  41. {mt5cli-1.7.0 → mt5cli-1.7.1}/LICENSE +0 -0
  42. {mt5cli-1.7.0 → mt5cli-1.7.1}/README.md +0 -0
  43. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/analytics.md +0 -0
  44. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/cli.md +0 -0
  45. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/client.md +0 -0
  46. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/converters.md +0 -0
  47. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/exceptions.md +0 -0
  48. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/history.md +0 -0
  49. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/index.md +0 -0
  50. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/marketdata.md +0 -0
  51. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/observability.md +0 -0
  52. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/public-contract.md +0 -0
  53. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/schemas.md +0 -0
  54. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/telemetry.md +0 -0
  55. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/trading.md +0 -0
  56. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/api/utils.md +0 -0
  57. {mt5cli-1.7.0 → mt5cli-1.7.1}/docs/index.md +0 -0
  58. {mt5cli-1.7.0 → mt5cli-1.7.1}/mkdocs.yml +0 -0
  59. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/__main__.py +0 -0
  60. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/contract.py +0 -0
  61. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/converters.py +0 -0
  62. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/exceptions.py +0 -0
  63. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/marketdata.py +0 -0
  64. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/observability.py +0 -0
  65. {mt5cli-1.7.0 → mt5cli-1.7.1}/mt5cli/retry.py +0 -0
  66. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/architecture/__init__.py +0 -0
  67. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/architecture/test_unit_module_correspondence.py +0 -0
  68. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/cli/test_cli.bats +0 -0
  69. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/contracts/__init__.py +0 -0
  70. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/contracts/test_public_api.py +0 -0
  71. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/docs/__init__.py +0 -0
  72. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/docs/test_public_contract.py +0 -0
  73. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/docs/test_rate_storage_contract.py +0 -0
  74. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/integration/__init__.py +0 -0
  75. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/integration/test_history_capture_persist.py +0 -0
  76. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/__init__.py +0 -0
  77. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/conftest.py +0 -0
  78. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_contract.py +0 -0
  79. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_converters.py +0 -0
  80. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_exceptions.py +0 -0
  81. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_lifecycle.py +0 -0
  82. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_rates.py +0 -0
  83. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_retry.py +0 -0
  84. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_schemas.py +0 -0
  85. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_sdk.py +0 -0
  86. {mt5cli-1.7.0 → mt5cli-1.7.1}/tests/unit/test_telemetry.py +0 -0
  87. {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=7
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
- npx -y prettier --write './**/*.{md,json}'
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: 7
9
+ default-days: 1
10
10
  open-pull-requests-limit: 10
11
- - package-ecosystem: pip
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: 7
20
+ default-days: 1
17
21
  open-pull-requests-limit: 10
22
+ groups:
23
+ uv-dependencies:
24
+ patterns:
25
+ - '*'
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
3
  "extends": ["config:recommended"],
4
- "minimumReleaseAge": "7 days",
4
+ "minimumReleaseAge": "1 day",
5
5
  "packageRules": [
6
6
  {
7
7
  "description": "Automatically merge minor and patch-level updates",
@@ -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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
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.0
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: noqa: F403
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"] # noqa: PLE0604
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
- conn.execute(f'DROP VIEW IF EXISTS {"temp." if temporary else ""}"{name}"')
102
- conn.execute(f'CREATE {"TEMP " if temporary else ""}VIEW "{name}" AS {select_sql}')
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," # noqa: S608
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," # noqa: S608
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}," # noqa: S608
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}"', # noqa: S608
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 # noqa: TC003
10
- from pathlib import Path # noqa: TC003
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 # noqa: PLC0415
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, Iterator, Sequence
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
- ) -> Iterator[Mt5DataClient]:
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
- """Return the underlying MT5 configuration."""
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: # noqa: ANN401
968
- """Return MT5 constants required by operational helpers.
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
- ) -> Iterator[MT5Client]:
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] # noqa: SLF001
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( # noqa: C901, PLR0914
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", # noqa: S608
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", # noqa: S608
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 ?", # noqa: S608
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
- """Return the timeframe as its integer MT5 value."""
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
- quoted_table = quote_sqlite_identifier(table)
585
- rows = conn.execute(f"PRAGMA table_info({quoted_table})").fetchall()
586
- return {str(row[1]) for row in rows}
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" # noqa: S608
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
- row = conn.execute(query, tuple(params)).fetchone()
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" # noqa: S608
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" # noqa: S608
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" # noqa: S608
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" # noqa: S608
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( # noqa: PLR0913
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( # noqa: PLR0913
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( # noqa: PLR0913
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( # noqa: PLR0913
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
- """Return the monotonic timestamp of the last successful update."""
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: # noqa: ANN401
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: noqa: PLR0913
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
- """ # noqa: S608
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 # noqa: PLC0415
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
- parsed = series.map(
255
- lambda value: _parse_mt5_time_value(value, unit=unit),
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
 
@@ -1,6 +1,6 @@
1
1
  """Authoritative stable package-root SDK surface for mt5cli."""
2
2
  # pyright: reportUnusedImport=false, reportUnsupportedDunderAll=false
3
- # ruff: noqa: F401, PLE0605
3
+ # ruff: file-ignore[unused-import, invalid-all-format]
4
4
 
5
5
  from pdmt5.dataframe import Mt5Config
6
6
 
@@ -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 Iterator
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: # noqa: ANN401
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: # noqa: ANN401
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
- ) -> Iterator[None]:
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) -> Iterator[None]:
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: # noqa: ANN401
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: