exness-data-preprocess 3.0.0__tar.gz → 3.0.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 (119) hide show
  1. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/.prototools +1 -1
  2. exness_data_preprocess-3.0.1/.python-version +1 -0
  3. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/CHANGELOG.md +7 -0
  4. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/CLAUDE.md +1 -1
  5. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/PKG-INFO +2 -1
  6. exness_data_preprocess-3.0.1/docs/release-notes/3.0.1.md +18 -0
  7. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/pyproject.toml +2 -9
  8. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/scripts/validate_clickhouse.py +46 -0
  9. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/clickhouse_manager.py +1 -1
  10. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/clickhouse_ohlc_generator.py +54 -47
  11. exness_data_preprocess-3.0.0/.python-version +0 -1
  12. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/.github/dependabot.yml +0 -0
  13. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/.github/workflows/publish.yml +0 -0
  14. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/.gitignore +0 -0
  15. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/.moon/toolchain.yml +0 -0
  16. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/.moon/workspace.yml +0 -0
  17. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/.pre-commit-config.yaml +0 -0
  18. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/.releaserc.yml +0 -0
  19. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/CONTRIBUTING.md +0 -0
  20. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/LICENSE +0 -0
  21. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/README.md +0 -0
  22. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/CLICKHOUSE_USER_GUIDE.md +0 -0
  23. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/EXNESS_DATA_SOURCES.md +0 -0
  24. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/README.md +0 -0
  25. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/adr/2025-12-09-codebase-pruning.md +0 -0
  26. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/adr/2025-12-09-exness-clickhouse-migration.md +0 -0
  27. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/adr/2025-12-10-clickhouse-e2e-validation-pipeline.md +0 -0
  28. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/adr/2025-12-10-clickhouse-pydantic-config.md +0 -0
  29. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/adr/2025-12-11-duckdb-removal-clickhouse.md +0 -0
  30. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/adr/diagrams/2025-12-10-clickhouse-pydantic-config-after.dot +0 -0
  31. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/adr/diagrams/2025-12-10-clickhouse-pydantic-config-arch.dot +0 -0
  32. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/adr/diagrams/2025-12-10-clickhouse-pydantic-config-before.dot +0 -0
  33. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/design/2025-12-09-codebase-pruning/spec.md +0 -0
  34. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/design/2025-12-09-exness-clickhouse-migration/spec.md +0 -0
  35. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/design/2025-12-10-clickhouse-e2e-validation-pipeline/spec.md +0 -0
  36. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/design/2025-12-10-clickhouse-pydantic-config/spec.md +0 -0
  37. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/design/2025-12-11-duckdb-removal-clickhouse/spec.md +0 -0
  38. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/release-notes/2.1.1.md +0 -0
  39. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/release-notes/3.0.0.md +0 -0
  40. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/HYBRID_SESSION_DETECTION_ANALYSIS.md +0 -0
  41. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/compression-benchmarks/README.md +0 -0
  42. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/compression-benchmarks/test_all_compression_methods.py +0 -0
  43. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/compression-benchmarks/test_delta_encoding_properly.py +0 -0
  44. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/compression-benchmarks/test_parquet_compression_methods.py +0 -0
  45. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/01-methodology.md +0 -0
  46. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/02-mode-truncated-analysis.md +0 -0
  47. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/03-hurdle-model-analysis.md +0 -0
  48. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/04-temporal-patterns.md +0 -0
  49. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/05-final-recommendations.md +0 -0
  50. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/README.md +0 -0
  51. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/scripts/reproduction_guide.md +0 -0
  52. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/visualizations/hurdle-model/hurdle_decomposition.png +0 -0
  53. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/visualizations/mode-truncated/cv_mode_comparison.png +0 -0
  54. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/visualizations/mode-truncated/distribution_overlay.png +0 -0
  55. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/visualizations/mode-truncated/signal_quality_comparison.png +0 -0
  56. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/visualizations/mode-truncated/variance_decomposition.png +0 -0
  57. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/visualizations/temporal/concentration_curve.png +0 -0
  58. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-spread-analysis/visualizations/temporal/percentile_analysis.png +0 -0
  59. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/01-methodology.md +0 -0
  60. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/02-baseline-sep2024.md +0 -0
  61. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/03-multiperiod-validation.md +0 -0
  62. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/04-discoveries-and-plan-evolution.md +0 -0
  63. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/README.md +0 -0
  64. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/findings/baseline-sep2024-comprehensive-summary.md +0 -0
  65. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/findings/phase2-mean-reversion-report.md +0 -0
  66. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/findings/phase3-volatility-model-report.md +0 -0
  67. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/findings/phase4-5-statistical-tests-survivorship-report.md +0 -0
  68. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/baseline-sep2024/01_mean_reversion_analysis.py +0 -0
  69. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/baseline-sep2024/02_volatility_model_simple.py +0 -0
  70. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/baseline-sep2024/03_liquidity_crisis_detection.py +0 -0
  71. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/baseline-sep2024/04_regime_detection_analysis.py +0 -0
  72. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase1_data_validation.py +0 -0
  73. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase2_mean_reversion.py +0 -0
  74. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase3_volatility_model.py +0 -0
  75. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase4_statistical_tests.py +0 -0
  76. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase5_survivorship_bias.py +0 -0
  77. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase5_survivorship_bias_v1.0.1.py +0 -0
  78. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase6a_interval_analysis.py +0 -0
  79. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase6b_price_clustering.py +0 -0
  80. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/multiperiod-validation/phase6c_combined_pattern.py +0 -0
  81. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/reproduction_guide.md +0 -0
  82. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/visualization/diagnostic_burst_decay.py +0 -0
  83. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/visualization/generate_interactive_dashboard.py +0 -0
  84. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/visualization/generate_lightweight_dashboard.py +0 -0
  85. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/docs/research/eurusd-zero-spread-deviations/scripts/visualization/prepare_visualization_data.py +0 -0
  86. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/examples/basic_usage.py +0 -0
  87. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/moon.yml +0 -0
  88. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/package-lock.json +0 -0
  89. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/package.json +0 -0
  90. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/scripts/clickhouse-start.sh +0 -0
  91. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/scripts/clickhouse-stop.sh +0 -0
  92. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/scripts/e2e-ephemeral.sh +0 -0
  93. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/scripts/generate_dbeaver_config.py +0 -0
  94. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/scripts/publish-release-notes.sh +0 -0
  95. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/scripts/smoke_install.sh +0 -0
  96. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/__init__.py +0 -0
  97. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/clickhouse_base.py +0 -0
  98. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/clickhouse_client.py +0 -0
  99. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/clickhouse_gap_detector.py +0 -0
  100. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/clickhouse_query_engine.py +0 -0
  101. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/downloader.py +0 -0
  102. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/exchanges.py +0 -0
  103. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/models.py +0 -0
  104. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/processor.py +0 -0
  105. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/session_detector.py +0 -0
  106. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/src/exness_data_preprocess/tick_loader.py +0 -0
  107. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/README.md +0 -0
  108. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/__init__.py +0 -0
  109. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/conftest.py +0 -0
  110. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_basic.py +0 -0
  111. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_database_bootstrap_order.py +0 -0
  112. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_date_bounds.py +0 -0
  113. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_functional_regression.py +0 -0
  114. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_models.py +0 -0
  115. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_package_structure.py +0 -0
  116. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_processor_pydantic.py +0 -0
  117. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_tick_seq.py +0 -0
  118. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/tests/test_types.py +0 -0
  119. {exness_data_preprocess-3.0.0 → exness_data_preprocess-3.0.1}/uv.toml +0 -0
@@ -2,4 +2,4 @@
2
2
  # uv owns Python: the interpreter version is .python-version, and uv.toml makes uv use only its own
3
3
  # managed build. ClickHouse comes from the system PATH (e.g. Homebrew), not from proto.
4
4
  uv = "latest"
5
- moon = "2.6.1"
5
+ moon = "2.6.2"
@@ -0,0 +1 @@
1
+ 3.15
@@ -1,3 +1,10 @@
1
+ ## [3.0.1](https://github.com/terrylica/exness-data-preprocess/compare/v3.0.0...v3.0.1) (2026-10-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **ohlc:** compute tick_count_standard and standard_spread_avg per minute from standard_ticks ([a26ec2c](https://github.com/terrylica/exness-data-preprocess/commit/a26ec2cca0e8e2b326c8b5c6f93f2c9f2a658986)), closes [#25](https://github.com/terrylica/exness-data-preprocess/issues/25)
7
+
1
8
  # [3.0.0](https://github.com/terrylica/exness-data-preprocess/compare/v2.1.1...v3.0.0) (2026-10-19)
2
9
 
3
10
 
@@ -16,7 +16,7 @@ moon run repo:build # sdist + wheel into dist/
16
16
 
17
17
  ## Toolchain
18
18
 
19
- - proto pins uv (`.prototools`); uv owns Python. `.python-version` pins 3.11 and wins. If `UV_PYTHON` is exported in the shell, it overrides the pin, so run `env -u UV_PYTHON uv ...` or the venv is built on the wrong interpreter.
19
+ - proto pins uv (`latest`) and moon (`.prototools`); uv owns Python. Development runs on the latest CPython (`.python-version`, currently 3.15); the published floor stays `requires-python >=3.11`, and ruff/mypy target 3.11 so code keeps working there. Never export `UV_PYTHON`: an env var outranks `.python-version`.
20
20
  - Releases: write `docs/release-notes/<next>.md` (narrative paragraph + Keep a Changelog bullets, one line each) in the PR before releasing; semantic-release (`npm run release`) refuses to release without it, runs `uv version <next> --frozen`, and `scripts/publish-release-notes.sh` rebuilds the GitHub Release body as narrative + generated section. pyproject.toml is the only place the version lives; `__version__` reads installed metadata. Publishing is `.github/workflows/publish.yml` (PyPI Trusted Publishing on a GitHub Release), the only publish path.
21
21
  - Dependency update PRs come from `.github/dependabot.yml`. No other CI runs in Actions; gates are local.
22
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: exness-data-preprocess
3
- Version: 3.0.0
3
+ Version: 3.0.1
4
4
  Summary: Professional Exness forex tick data preprocessing with ClickHouse backend. Provides efficient storage with lossless precision and direct queryability.
5
5
  Project-URL: Homepage, https://github.com/terrylica/exness-data-preprocess
6
6
  Project-URL: Documentation, https://github.com/terrylica/exness-data-preprocess#readme
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3
22
22
  Classifier: Programming Language :: Python :: 3.11
23
23
  Classifier: Programming Language :: Python :: 3.12
24
24
  Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.15
25
26
  Classifier: Topic :: Office/Business :: Financial :: Investment
26
27
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
27
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -0,0 +1,18 @@
1
+ ## 3.0.1 (2026-10-19)
2
+
3
+ Two `ohlc_1m` columns did not measure what their documentation says. `tick_count_standard` is documented as the number of Standard ticks in the bar, but it always equalled the Raw_Spread count: it was computed as `countIf(... IS NOT NULL)` over an ASOF join that never produces NULLs. `standard_spread_avg` is documented as the average Standard spread in the bar, but it averaged whichever Standard quote preceded each Raw_Spread tick, and the choice among tied quotes could change between rebuilds. Each variant is now aggregated per minute on its own and the two are joined on the minute, so both columns match their definitions, rebuilds are deterministic, and the build no longer runs a per-tick join. On the E2E months the counts now differ on 64,889 of 78,658 bars, where before they were identical on every bar.
4
+
5
+ ### Upgrading
6
+
7
+ - Bars already stored keep the old values until they are rebuilt. Rebuild an instrument's bars with `update_data(pair, start_date=..., force_redownload=True)`, which re-downloads and regenerates from `start_date`, or call `ExnessDataProcessor().ch_ohlc_generator.regenerate_ohlc(pair)` to rebuild every bar from the stored ticks.
8
+
9
+ ### Fixed
10
+
11
+ - `tick_count_standard` counts the Standard ticks in the bar's minute (0 when there are none) (#25).
12
+ - `standard_spread_avg` is the mean Standard `ask - bid` in the bar's minute (NULL when there are none), deterministic across rebuilds (#25).
13
+ - `repo:e2e` checks that `sum(tick_count_raw_spread)` equals the stored Raw_Spread ticks and `sum(tick_count_standard)` equals the Standard ticks in bar minutes, that the two counts differ, and that a sampled `standard_spread_avg` equals a direct per-minute average (#25).
14
+ - Stale docstrings describing the v1 30-column schema, normalized metrics and the ASOF join are corrected (#25).
15
+
16
+ ### Changed
17
+
18
+ - Development runs on Python 3.15.0 and moon 2.6.2; the supported range is unchanged (`requires-python >=3.11`) (#27, #28).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "exness-data-preprocess"
3
- version = "3.0.0"
3
+ version = "3.0.1"
4
4
  description = "Professional Exness forex tick data preprocessing with ClickHouse backend. Provides efficient storage with lossless precision and direct queryability."
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
6
  authors = [
@@ -26,6 +26,7 @@ classifiers = [
26
26
  "Programming Language :: Python :: 3.11",
27
27
  "Programming Language :: Python :: 3.12",
28
28
  "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.15",
29
30
  "Topic :: Office/Business :: Financial :: Investment",
30
31
  "Topic :: Scientific/Engineering :: Information Analysis",
31
32
  "Topic :: Software Development :: Libraries :: Python Modules",
@@ -81,10 +82,6 @@ ignore = [
81
82
  [tool.ruff.lint.isort]
82
83
  known-first-party = ["exness_data_preprocess"]
83
84
 
84
- [tool.black]
85
- target-version = ['py311']
86
- line-length = 100
87
-
88
85
  [tool.mypy]
89
86
  python_version = "3.11"
90
87
  warn_return_any = true
@@ -97,10 +94,6 @@ no_implicit_optional = true
97
94
  strict_optional = true
98
95
  ignore_missing_imports = true
99
96
 
100
- [[tool.mypy.overrides]]
101
- module = "tests.*"
102
- ignore_errors = true
103
-
104
97
  [tool.pytest.ini_options]
105
98
  minversion = "7.0"
106
99
  addopts = "-ra -q --strict-markers"
@@ -233,6 +233,51 @@ def check_ties() -> None:
233
233
  print("✅ Tie pagination passed")
234
234
 
235
235
 
236
+ def check_ohlc_counts() -> None:
237
+ """ohlc_1m tick counts and Standard spread match their definitions (per minute, per variant)."""
238
+ print("🔍 Validating OHLC tick counts and Standard spread...")
239
+ from exness_data_preprocess import get_clickhouse_client
240
+
241
+ client = get_clickhouse_client()
242
+ try:
243
+ sum_std, sum_raw, differing = client.query(
244
+ "SELECT sum(tick_count_standard), sum(tick_count_raw_spread),"
245
+ " countIf(tick_count_standard != tick_count_raw_spread)"
246
+ " FROM exness.ohlc_1m FINAL WHERE instrument = 'EURUSD'"
247
+ ).first_row
248
+ raw_total = client.query(
249
+ "SELECT count() FROM exness.raw_spread_ticks FINAL WHERE instrument = 'EURUSD'"
250
+ ).first_row[0]
251
+ std_in_bars = client.query(
252
+ "SELECT count() FROM exness.standard_ticks FINAL WHERE instrument = 'EURUSD'"
253
+ " AND toStartOfMinute(timestamp) IN"
254
+ " (SELECT timestamp FROM exness.ohlc_1m FINAL WHERE instrument = 'EURUSD')"
255
+ ).first_row[0]
256
+ minute, bar_avg, direct_avg = client.query(
257
+ "SELECT o.timestamp, o.standard_spread_avg, d.avg_spread FROM exness.ohlc_1m AS o FINAL"
258
+ " INNER JOIN (SELECT toStartOfMinute(timestamp) AS m, avg(ask - bid) AS avg_spread, count() AS n"
259
+ " FROM exness.standard_ticks FINAL WHERE instrument = 'EURUSD' GROUP BY m"
260
+ " ORDER BY n DESC, m LIMIT 1) AS d ON o.timestamp = d.m WHERE o.instrument = 'EURUSD'"
261
+ ).first_row
262
+ finally:
263
+ client.close()
264
+ assert sum_raw == raw_total, (
265
+ f"sum(tick_count_raw_spread)={sum_raw:,} != stored Raw_Spread ticks {raw_total:,}"
266
+ )
267
+ assert sum_std == std_in_bars, (
268
+ f"sum(tick_count_standard)={sum_std:,} != Standard ticks in bar minutes {std_in_bars:,}"
269
+ )
270
+ assert differing > 0, "tick_count_standard equals tick_count_raw_spread on every bar"
271
+ assert abs(bar_avg - direct_avg) < 1e-12, (
272
+ f"standard_spread_avg {bar_avg} != direct {direct_avg} at {minute}"
273
+ )
274
+ print(
275
+ f" ✓ sum(tick_count_raw_spread)={sum_raw:,} = stored; sum(tick_count_standard)={sum_std:,} ="
276
+ f" Standard ticks in bar minutes; counts differ on {differing:,} bars; spread avg exact at {minute}"
277
+ )
278
+ print("✅ OHLC count validation passed")
279
+
280
+
236
281
  def check_data() -> None:
237
282
  print("🔍 Validating ClickHouse data operations...")
238
283
  from exness_data_preprocess import ClickHouseQueryEngine
@@ -318,6 +363,7 @@ CHECKS: dict[str, Callable[[], None]] = {
318
363
  "pagination": check_pagination,
319
364
  "ties": check_ties,
320
365
  "ohlc": check_ohlc,
366
+ "ohlc_counts": check_ohlc_counts,
321
367
  }
322
368
 
323
369
 
@@ -242,7 +242,7 @@ class ClickHouseManager(ClickHouseClientMixin):
242
242
  "standard_ticks",
243
243
  "Standard tick data from Exness. Columns: instrument (forex pair), timestamp "
244
244
  "(microsecond), seq (position among same-timestamp ticks in source order), bid/ask "
245
- "(traditional quotes, Bid < Ask). ASOF merged with raw_spread_ticks.",
245
+ "(traditional quotes, Bid < Ask). Aggregated per minute into ohlc_1m.",
246
246
  )
247
247
 
248
248
  def _create_ticks_table(self, table: str, comment: str) -> None:
@@ -5,15 +5,14 @@ ADR: /docs/adr/2025-12-09-exness-clickhouse-migration.md
5
5
 
6
6
  SLOs:
7
7
  - Availability: OHLC generation must succeed or raise exception (no silent failures)
8
- - Correctness: Phase7 30-column schema with dual variants, normalized metrics, holiday/session detection
8
+ - Correctness: 27-column ohlc_1m with dual-variant spreads and tick counts, holiday/session detection
9
9
  - Observability: All OHLC operations logged via structured logging
10
10
  - Maintainability: Single module for all OHLC logic, off-the-shelf ClickHouse and exchange_calendars
11
11
 
12
12
  Handles:
13
- - Phase7 30-column OHLC schema
13
+ - 27-column OHLC schema (ohlc_1m)
14
14
  - BID-only OHLC construction from raw_spread_ticks
15
- - ASOF JOIN with standard_ticks for dual spread tracking
16
- - Normalized metrics (range_per_spread, range_per_tick, body_per_spread, body_per_tick)
15
+ - Per-minute Standard spread and tick count from standard_ticks, joined on the minute
17
16
  - Timezone/session tracking (NY and London sessions)
18
17
  - Holiday detection (US, UK, major holidays via exchange_calendars)
19
18
  - 10 global exchange session flags with trading hour detection
@@ -33,12 +32,11 @@ from exness_data_preprocess.session_detector import SessionDetector
33
32
 
34
33
  class ClickHouseOHLCGenerator(ClickHouseClientMixin):
35
34
  """
36
- Generate Phase7 30-column OHLC from dual-variant tick data in ClickHouse.
35
+ Generate 27-column 1-minute OHLC from dual-variant tick data in ClickHouse.
37
36
 
38
37
  Responsibilities:
39
38
  - Delete existing OHLC data before regeneration (by instrument)
40
- - Generate BID-only OHLC from raw_spread_ticks with ASOF JOIN to standard_ticks
41
- - Calculate normalized spread metrics (NULL-safe)
39
+ - Generate BID-only OHLC from raw_spread_ticks plus per-minute Standard aggregates
42
40
  - Add timezone/session tracking for NY and London
43
41
  - Delegate holiday and session detection to SessionDetector
44
42
  - Insert OHLC data with holiday and session flags
@@ -69,11 +67,12 @@ class ClickHouseOHLCGenerator(ClickHouseClientMixin):
69
67
  end_date: str | None = None,
70
68
  ) -> int:
71
69
  """
72
- Generate/update OHLC table with Phase7 schema (30 columns).
70
+ Generate/update ohlc_1m (27 columns).
73
71
 
74
- Uses ASOF JOIN to combine raw_spread_ticks and standard_ticks.
75
- Includes normalized spread metrics, timezone/session tracking,
76
- holiday detection, and 10 global exchange session flags.
72
+ Raw_Spread and Standard ticks are each aggregated per minute and joined on the minute:
73
+ standard_spread_avg and tick_count_standard describe the Standard ticks in that minute
74
+ (NULL / 0 when there are none). Includes timezone/session tracking, holiday detection,
75
+ and 10 global exchange session flags.
77
76
 
78
77
  Supports three operation modes:
79
78
  1. Full regeneration (start_date=None, end_date=None): DELETE all + INSERT all
@@ -143,44 +142,42 @@ class ClickHouseOHLCGenerator(ClickHouseClientMixin):
143
142
  )
144
143
 
145
144
  where_sql = " AND ".join(where_parts)
146
- standard_where = ["instrument = {instrument:String}"]
147
- if start_date:
148
- standard_where.append("timestamp >= {start_date:DateTime64(6, 'UTC')} - INTERVAL 7 DAY")
149
- standard_where_sql = " AND ".join(standard_where)
150
145
 
151
146
  # Generate session column initializations from exchange registry
152
147
  session_columns = ", ".join([f"0 AS is_{name}_session" for name in EXCHANGES.keys()])
153
148
 
154
- # Generate OHLC with 27-column schema (matches DATABASE_SCHEMA.md)
155
- # Note: ClickHouse ASOF JOIN matches closest preceding timestamp from standard_ticks
149
+ # Each variant is aggregated per minute on its own and the two are joined on the minute.
150
+ # Bars exist for minutes with Raw_Spread ticks; standard_spread_avg / tick_count_standard
151
+ # describe the Standard ticks inside the same minute (NULL / 0 when there are none).
152
+ # Both subqueries apply the same instrument and date filter (alias r in each).
156
153
  insert_sql = f"""
157
154
  INSERT INTO {self.DATABASE}.ohlc_1m
158
155
  SELECT
159
156
  {{instrument:String}} AS instrument,
160
- toStartOfMinute(r.timestamp) AS timestamp,
161
- argMin(r.bid, (r.timestamp, r.seq)) AS open,
162
- max(r.bid) AS high,
163
- min(r.bid) AS low,
164
- argMax(r.bid, (r.timestamp, r.seq)) AS close,
165
- avg(r.ask - r.bid) AS raw_spread_avg,
166
- avg(s.ask - s.bid) AS standard_spread_avg,
167
- toUInt32(count(r.timestamp)) AS tick_count_raw_spread,
168
- toUInt32(countIf(s.timestamp IS NOT NULL)) AS tick_count_standard,
157
+ r.minute AS timestamp,
158
+ r.open AS open,
159
+ r.high AS high,
160
+ r.low AS low,
161
+ r.close AS close,
162
+ r.raw_spread_avg AS raw_spread_avg,
163
+ s.standard_spread_avg AS standard_spread_avg,
164
+ r.tick_count_raw_spread AS tick_count_raw_spread,
165
+ ifNull(s.tick_count_standard, toUInt32(0)) AS tick_count_standard,
169
166
  -- Timezone-aware session columns
170
- toUInt8(toHour(toTimezone(toStartOfMinute(r.timestamp), 'America/New_York'))) AS ny_hour,
171
- toUInt8(toHour(toTimezone(toStartOfMinute(r.timestamp), 'Europe/London'))) AS london_hour,
167
+ toUInt8(toHour(toTimezone(r.minute, 'America/New_York'))) AS ny_hour,
168
+ toUInt8(toHour(toTimezone(r.minute, 'Europe/London'))) AS london_hour,
172
169
  multiIf(
173
- toHour(toTimezone(toStartOfMinute(r.timestamp), 'America/New_York')) >= 9
174
- AND toHour(toTimezone(toStartOfMinute(r.timestamp), 'America/New_York')) <= 16,
170
+ toHour(toTimezone(r.minute, 'America/New_York')) >= 9
171
+ AND toHour(toTimezone(r.minute, 'America/New_York')) <= 16,
175
172
  'NY_Session',
176
- toHour(toTimezone(toStartOfMinute(r.timestamp), 'America/New_York')) >= 17
177
- AND toHour(toTimezone(toStartOfMinute(r.timestamp), 'America/New_York')) <= 20,
173
+ toHour(toTimezone(r.minute, 'America/New_York')) >= 17
174
+ AND toHour(toTimezone(r.minute, 'America/New_York')) <= 20,
178
175
  'NY_After_Hours',
179
176
  'NY_Closed'
180
177
  ) AS ny_session,
181
178
  if(
182
- toHour(toTimezone(toStartOfMinute(r.timestamp), 'Europe/London')) >= 8
183
- AND toHour(toTimezone(toStartOfMinute(r.timestamp), 'Europe/London')) <= 16,
179
+ toHour(toTimezone(r.minute, 'Europe/London')) >= 8
180
+ AND toHour(toTimezone(r.minute, 'Europe/London')) <= 16,
184
181
  'London_Session',
185
182
  'London_Closed'
186
183
  ) AS london_session,
@@ -190,20 +187,30 @@ class ClickHouseOHLCGenerator(ClickHouseClientMixin):
190
187
  toUInt8(0) AS is_major_holiday,
191
188
  -- Trading session flags (initialized to 0, updated via session_detector)
192
189
  {session_columns}
193
- FROM {self.DATABASE}.raw_spread_ticks AS r FINAL
194
- ASOF LEFT JOIN (
195
- -- Only this instrument, and only from a week before start_date: enough for the
196
- -- closest preceding Standard tick across a weekend, without reading (and FINAL-
197
- -- merging) every instrument and month on the join's build side.
198
- SELECT instrument, timestamp, ask, bid
199
- FROM {self.DATABASE}.standard_ticks FINAL
200
- WHERE {standard_where_sql}
201
- ) AS s
202
- ON r.instrument = s.instrument
203
- AND r.timestamp >= s.timestamp
204
- WHERE {where_sql}
205
- GROUP BY toStartOfMinute(r.timestamp)
190
+ FROM (
191
+ SELECT
192
+ toStartOfMinute(r.timestamp) AS minute,
193
+ argMin(r.bid, (r.timestamp, r.seq)) AS open,
194
+ max(r.bid) AS high,
195
+ min(r.bid) AS low,
196
+ argMax(r.bid, (r.timestamp, r.seq)) AS close,
197
+ avg(r.ask - r.bid) AS raw_spread_avg,
198
+ toUInt32(count()) AS tick_count_raw_spread
199
+ FROM {self.DATABASE}.raw_spread_ticks AS r FINAL
200
+ WHERE {where_sql}
201
+ GROUP BY minute
202
+ ) AS r
203
+ LEFT JOIN (
204
+ SELECT
205
+ toStartOfMinute(r.timestamp) AS minute,
206
+ avg(r.ask - r.bid) AS standard_spread_avg,
207
+ toUInt32(count()) AS tick_count_standard
208
+ FROM {self.DATABASE}.standard_ticks AS r FINAL
209
+ WHERE {where_sql}
210
+ GROUP BY minute
211
+ ) AS s ON r.minute = s.minute
206
212
  ORDER BY timestamp
213
+ SETTINGS join_use_nulls = 1
207
214
  """
208
215
 
209
216
  execute_command(self.client, insert_sql, parameters=params)
@@ -1 +0,0 @@
1
- 3.11