bquant 0.0.1__tar.gz → 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. {bquant-0.0.1 → bquant-0.0.2}/CHANGELOG.md +17 -0
  2. {bquant-0.0.1/bquant.egg-info → bquant-0.0.2}/PKG-INFO +5 -1
  3. {bquant-0.0.1 → bquant-0.0.2}/bquant/__init__.py +1 -1
  4. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/models.py +15 -0
  5. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/pipeline.py +12 -5
  6. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/presets.py +18 -5
  7. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/zigzag.py +85 -20
  8. bquant-0.0.2/bquant/visualization/utils.py +211 -0
  9. {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/zones.py +153 -125
  10. {bquant-0.0.1 → bquant-0.0.2/bquant.egg-info}/PKG-INFO +5 -1
  11. {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/SOURCES.txt +6 -4
  12. {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/requires.txt +5 -0
  13. {bquant-0.0.1 → bquant-0.0.2}/docs/README.md +28 -0
  14. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/zones/pipeline.md.txt +3 -3
  15. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/zone_analyzer_deep_dive.md.txt +103 -15
  16. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/index.rst.txt +2 -1
  17. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/migration/global_swings_migration.md.txt +13 -7
  18. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/README.md.txt +6 -0
  19. bquant-0.0.2/docs/_build/html/_sources/user_guide/caching.md.txt +189 -0
  20. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/swing_strategies.md.txt +1 -1
  21. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/zone_analysis.md.txt +84 -59
  22. bquant-0.0.2/docs/analytics/zones/layerA_ii_representations_case_study.md +68 -0
  23. bquant-0.0.2/docs/analytics/zones/layerA_zone_structure_case_study.md +73 -0
  24. {bquant-0.0.1 → bquant-0.0.2}/docs/analytics/zones/macd_zone_consistency_case_study.md +29 -0
  25. bquant-0.0.2/docs/analytics/zones/s1_swing_anatomy_case_study.md +127 -0
  26. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/zones/pipeline.md +3 -3
  27. {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/zone_analyzer_deep_dive.md +103 -15
  28. {bquant-0.0.1 → bquant-0.0.2}/docs/index.rst +2 -1
  29. {bquant-0.0.1 → bquant-0.0.2}/docs/migration/global_swings_migration.md +13 -7
  30. {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/README.md +12 -0
  31. {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/best_practices.md +4 -1
  32. bquant-0.0.2/docs/user_guide/caching.md +189 -0
  33. {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/swing_strategies.md +1 -1
  34. {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/zone_analysis.md +86 -59
  35. bquant-0.0.2/docs/user_guide/zone_analysis_result.md +442 -0
  36. {bquant-0.0.1 → bquant-0.0.2}/examples/04_comprehensive_analysis.py +1 -1
  37. {bquant-0.0.1 → bquant-0.0.2}/examples/08_macd_swing_analysis.py +1 -1
  38. {bquant-0.0.1 → bquant-0.0.2}/pyproject.toml +6 -1
  39. {bquant-0.0.1 → bquant-0.0.2}/scripts/analysis/batch_analysis.py +9 -1
  40. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_strategy_infrastructure.py +22 -1
  41. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_swing_global_calculation.py +22 -0
  42. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zigzag_swing_strategy.py +60 -0
  43. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_presets.py +2 -2
  44. bquant-0.0.1/bquant/indicators/base_old.py +0 -664
  45. bquant-0.0.1/bquant/indicators/library.py +0 -514
  46. bquant-0.0.1/bquant/visualization/utils.py +0 -41
  47. {bquant-0.0.1 → bquant-0.0.2}/LICENSE +0 -0
  48. {bquant-0.0.1 → bquant-0.0.2}/MANIFEST.in +0 -0
  49. {bquant-0.0.1 → bquant-0.0.2}/README.md +0 -0
  50. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/__init__.py +0 -0
  51. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/candlestick/__init__.py +0 -0
  52. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/chart/__init__.py +0 -0
  53. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/statistical/__init__.py +0 -0
  54. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/statistical/hypothesis_testing.py +0 -0
  55. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/statistical/regression.py +0 -0
  56. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/technical/__init__.py +0 -0
  57. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/timeseries/__init__.py +0 -0
  58. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/validation/__init__.py +0 -0
  59. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/validation/suite.py +0 -0
  60. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/__init__.py +0 -0
  61. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/analyzer.py +0 -0
  62. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/cache.py +0 -0
  63. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/__init__.py +0 -0
  64. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/base.py +0 -0
  65. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/combined.py +0 -0
  66. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/line_crossing.py +0 -0
  67. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/preloaded.py +0 -0
  68. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/registry.py +0 -0
  69. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/threshold.py +0 -0
  70. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/zero_crossing.py +0 -0
  71. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/sequence_analysis.py +0 -0
  72. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/__init__.py +0 -0
  73. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/base.py +0 -0
  74. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/divergence/__init__.py +0 -0
  75. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/divergence/classic.py +0 -0
  76. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/registry.py +0 -0
  77. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/shape/__init__.py +0 -0
  78. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/shape/statistical.py +0 -0
  79. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/__init__.py +0 -0
  80. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/find_peaks.py +0 -0
  81. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/pivot_points.py +0 -0
  82. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/thresholds.py +0 -0
  83. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/volatility/__init__.py +0 -0
  84. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/volatility/combined.py +0 -0
  85. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/volume/__init__.py +0 -0
  86. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/volume/standard.py +0 -0
  87. {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/zone_features.py +0 -0
  88. {bquant-0.0.1 → bquant-0.0.2}/bquant/cli.py +0 -0
  89. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/__init__.py +0 -0
  90. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/cache.py +0 -0
  91. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/config.py +0 -0
  92. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/exceptions.py +0 -0
  93. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/logging_config.py +0 -0
  94. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/nb.py +0 -0
  95. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/numpy_fix.py +0 -0
  96. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/performance.py +0 -0
  97. {bquant-0.0.1 → bquant-0.0.2}/bquant/core/utils.py +0 -0
  98. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/__init__.py +0 -0
  99. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/loader.py +0 -0
  100. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/processor.py +0 -0
  101. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/__init__.py +0 -0
  102. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/datasets.py +0 -0
  103. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/embedded/__init__.py +0 -0
  104. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/embedded/mt_xauusd_m15.py +0 -0
  105. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/embedded/tv_xauusd_1h.py +0 -0
  106. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/generator.py +0 -0
  107. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/utils.py +0 -0
  108. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/schemas.py +0 -0
  109. {bquant-0.0.1 → bquant-0.0.2}/bquant/data/validator.py +0 -0
  110. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/__init__.py +0 -0
  111. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/base.py +0 -0
  112. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/calculators.py +0 -0
  113. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/__init__.py +0 -0
  114. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/bollinger.py +0 -0
  115. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/ema.py +0 -0
  116. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/macd.py +0 -0
  117. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/rsi.py +0 -0
  118. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/sma.py +0 -0
  119. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/library/__init__.py +0 -0
  120. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/library/manager.py +0 -0
  121. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/library/pandas_ta.py +0 -0
  122. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/library/talib.py +0 -0
  123. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/macd.py +0 -0
  124. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/preloaded/__init__.py +0 -0
  125. {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/preloaded/macd.py +0 -0
  126. {bquant-0.0.1 → bquant-0.0.2}/bquant/ml/__init__.py +0 -0
  127. {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/__init__.py +0 -0
  128. {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/charts.py +0 -0
  129. {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/export.py +0 -0
  130. {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/statistical.py +0 -0
  131. {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/themes.py +0 -0
  132. {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/dependency_links.txt +0 -0
  133. {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/entry_points.txt +0 -0
  134. {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/top_level.txt +0 -0
  135. {bquant-0.0.1 → bquant-0.0.2}/docs/BUILD_ISSUES.md +0 -0
  136. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/analytics/zones/macd_zone_consistency_case_study.md.txt +0 -0
  137. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/analytics/zones/swing.md.txt +0 -0
  138. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/analytics/zones/swing_strategy_comparison_case_study.md.txt +0 -0
  139. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/README.md.txt +0 -0
  140. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/README.md.txt +0 -0
  141. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/base.md.txt +0 -0
  142. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/pipeline.md.txt +0 -0
  143. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/statistical.md.txt +0 -0
  144. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/strategies.md.txt +0 -0
  145. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/zones/models.md.txt +0 -0
  146. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/zones/strategies.md.txt +0 -0
  147. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/zones.md.txt +0 -0
  148. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/README.md.txt +0 -0
  149. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/config.md.txt +0 -0
  150. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/exceptions.md.txt +0 -0
  151. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/logging.md.txt +0 -0
  152. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/nb.md.txt +0 -0
  153. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/performance.md.txt +0 -0
  154. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/utils.md.txt +0 -0
  155. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/README.md.txt +0 -0
  156. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/loader.md.txt +0 -0
  157. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/processor.md.txt +0 -0
  158. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/samples.md.txt +0 -0
  159. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/schemas.md.txt +0 -0
  160. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/validator.md.txt +0 -0
  161. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/extension_guide.md.txt +0 -0
  162. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/README.md.txt +0 -0
  163. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/base.md.txt +0 -0
  164. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/factory.md.txt +0 -0
  165. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/library_manager.md.txt +0 -0
  166. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/macd.md.txt +0 -0
  167. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/preloaded.md.txt +0 -0
  168. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/visualization/README.md.txt +0 -0
  169. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/visualization/zones.md.txt +0 -0
  170. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/README.md.txt +0 -0
  171. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/analytical_philosophy.md.txt +0 -0
  172. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/statistical_analysis_workflow.md.txt +0 -0
  173. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/zone_detection_strategies.md.txt +0 -0
  174. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/examples/README.md.txt +0 -0
  175. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/migration/MIGRATION_v2.md.txt +0 -0
  176. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/README.md.txt +0 -0
  177. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/combined_rules_detection.md.txt +0 -0
  178. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/macd_basic_pipeline.md.txt +0 -0
  179. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/preloaded_zones_workflow.md.txt +0 -0
  180. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/rsi_strategy_switching.md.txt +0 -0
  181. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/best_practices.md.txt +0 -0
  182. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/core_concepts.md.txt +0 -0
  183. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/quick_start.md.txt +0 -0
  184. {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/swing_analysis_results.md.txt +0 -0
  185. {bquant-0.0.1 → bquant-0.0.2}/docs/analytics/zones/swing.md +0 -0
  186. {bquant-0.0.1 → bquant-0.0.2}/docs/analytics/zones/swing_strategy_comparison_case_study.md +0 -0
  187. {bquant-0.0.1 → bquant-0.0.2}/docs/api/README.md +0 -0
  188. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/README.md +0 -0
  189. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/base.md +0 -0
  190. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/pipeline.md +0 -0
  191. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/statistical.md +0 -0
  192. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/strategies.md +0 -0
  193. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/zones/models.md +0 -0
  194. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/zones/strategies.md +0 -0
  195. {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/zones.md +0 -0
  196. {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/README.md +0 -0
  197. {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/config.md +0 -0
  198. {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/exceptions.md +0 -0
  199. {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/logging.md +0 -0
  200. {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/nb.md +0 -0
  201. {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/performance.md +0 -0
  202. {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/utils.md +0 -0
  203. {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/README.md +0 -0
  204. {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/loader.md +0 -0
  205. {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/processor.md +0 -0
  206. {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/samples.md +0 -0
  207. {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/schemas.md +0 -0
  208. {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/validator.md +0 -0
  209. {bquant-0.0.1 → bquant-0.0.2}/docs/api/extension_guide.md +0 -0
  210. {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/README.md +0 -0
  211. {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/base.md +0 -0
  212. {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/factory.md +0 -0
  213. {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/library_manager.md +0 -0
  214. {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/macd.md +0 -0
  215. {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/preloaded.md +0 -0
  216. {bquant-0.0.1 → bquant-0.0.2}/docs/api/visualization/README.md +0 -0
  217. {bquant-0.0.1 → bquant-0.0.2}/docs/api/visualization/zones.md +0 -0
  218. {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/README.md +0 -0
  219. {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/analytical_philosophy.md +0 -0
  220. {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/statistical_analysis_workflow.md +0 -0
  221. {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/zone_detection_strategies.md +0 -0
  222. {bquant-0.0.1 → bquant-0.0.2}/docs/examples/README.md +0 -0
  223. {bquant-0.0.1 → bquant-0.0.2}/docs/migration/MIGRATION_v2.md +0 -0
  224. {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/README.md +0 -0
  225. {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/combined_rules_detection.md +0 -0
  226. {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/macd_basic_pipeline.md +0 -0
  227. {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/preloaded_zones_workflow.md +0 -0
  228. {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/rsi_strategy_switching.md +0 -0
  229. {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/core_concepts.md +0 -0
  230. {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/quick_start.md +0 -0
  231. {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/swing_analysis_results.md +0 -0
  232. {bquant-0.0.1 → bquant-0.0.2}/examples/01_basic_indicators.py +0 -0
  233. {bquant-0.0.1 → bquant-0.0.2}/examples/02_macd_zone_analysis.py +0 -0
  234. {bquant-0.0.1 → bquant-0.0.2}/examples/02a_universal_zones.py +0 -0
  235. {bquant-0.0.1 → bquant-0.0.2}/examples/03_data_processing.py +0 -0
  236. {bquant-0.0.1 → bquant-0.0.2}/examples/05_strategies_demo.py +0 -0
  237. {bquant-0.0.1 → bquant-0.0.2}/examples/06_regression_demo.py +0 -0
  238. {bquant-0.0.1 → bquant-0.0.2}/examples/07_validation_demo.py +0 -0
  239. {bquant-0.0.1 → bquant-0.0.2}/examples/09_zones_visualization_demo.py +0 -0
  240. {bquant-0.0.1 → bquant-0.0.2}/examples/README.md +0 -0
  241. {bquant-0.0.1 → bquant-0.0.2}/examples/zone_analysis_global_swings.py +0 -0
  242. {bquant-0.0.1 → bquant-0.0.2}/scripts/README.md +0 -0
  243. {bquant-0.0.1 → bquant-0.0.2}/scripts/analysis/README.md +0 -0
  244. {bquant-0.0.1 → bquant-0.0.2}/scripts/analysis/run_macd_analysis.py +0 -0
  245. {bquant-0.0.1 → bquant-0.0.2}/scripts/analysis/test_hypotheses.py +0 -0
  246. {bquant-0.0.1 → bquant-0.0.2}/scripts/data/data_loader.py +0 -0
  247. {bquant-0.0.1 → bquant-0.0.2}/scripts/data/extract_samples.py +0 -0
  248. {bquant-0.0.1 → bquant-0.0.2}/scripts/data/generate_samples.py +0 -0
  249. {bquant-0.0.1 → bquant-0.0.2}/scripts/data_processing/README.md +0 -0
  250. {bquant-0.0.1 → bquant-0.0.2}/scripts/deployment/README.md +0 -0
  251. {bquant-0.0.1 → bquant-0.0.2}/scripts/publishing/README.md +0 -0
  252. {bquant-0.0.1 → bquant-0.0.2}/setup.cfg +0 -0
  253. {bquant-0.0.1 → bquant-0.0.2}/tests/analysis/zones/test_pipeline_cache.py +0 -0
  254. {bquant-0.0.1 → bquant-0.0.2}/tests/analysis/zones/test_swing_presets.py +0 -0
  255. {bquant-0.0.1 → bquant-0.0.2}/tests/analysis/zones/test_swing_thresholds.py +0 -0
  256. {bquant-0.0.1 → bquant-0.0.2}/tests/conftest.py +0 -0
  257. {bquant-0.0.1 → bquant-0.0.2}/tests/fixtures/__init__.py +0 -0
  258. {bquant-0.0.1 → bquant-0.0.2}/tests/fixtures/swing_mocks.py +0 -0
  259. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/__init__.py +0 -0
  260. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_backward_compatibility.py +0 -0
  261. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_data_pipeline.py +0 -0
  262. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_full_pipeline.py +0 -0
  263. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_pipeline_global_swings.py +0 -0
  264. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_scripts_integration.py +0 -0
  265. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_truly_universal_zones.py +0 -0
  266. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_visualization_pipeline.py +0 -0
  267. {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_zone_analysis_e2e.py +0 -0
  268. {bquant-0.0.1 → bquant-0.0.2}/tests/performance/test_swing_performance.py +0 -0
  269. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/__init__.py +0 -0
  270. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_all_indicator_types.py +0 -0
  271. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_analysis_structure.py +0 -0
  272. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_architecture_validation.py +0 -0
  273. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_builtin_migration.py +0 -0
  274. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_cache_versioning.py +0 -0
  275. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_classic_divergence_strategy.py +0 -0
  276. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_combined_volatility_strategy.py +0 -0
  277. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_compatibility_validation.py +0 -0
  278. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_core_modules.py +0 -0
  279. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_data_modules.py +0 -0
  280. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_divergence_strategy_universal.py +0 -0
  281. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_find_peaks_swing_strategy.py +0 -0
  282. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_imports_migration.py +0 -0
  283. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_indicator_factory.py +0 -0
  284. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_indicators_base.py +0 -0
  285. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_library_indicators.py +0 -0
  286. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_macd_analyzer.py +0 -0
  287. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_macd_backward_compatibility.py +0 -0
  288. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_pandas_ta_dynamic_loader.py +0 -0
  289. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_performance.py +0 -0
  290. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_pivot_points_swing_strategy.py +0 -0
  291. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_preloaded_indicators.py +0 -0
  292. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_sample_data.py +0 -0
  293. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_sample_generator.py +0 -0
  294. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_shape_strategy_universal.py +0 -0
  295. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_standard_volume_strategy.py +0 -0
  296. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_statistical_hypothesis.py +0 -0
  297. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_statistical_shape_strategy.py +0 -0
  298. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_swing_context_memory.py +0 -0
  299. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_swing_edge_cases.py +0 -0
  300. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_time_metrics.py +0 -0
  301. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_universal_zone_analyzer.py +0 -0
  302. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_validation_suite.py +0 -0
  303. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_visualization.py +0 -0
  304. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_volume_strategy_universal.py +0 -0
  305. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_analysis_builder.py +0 -0
  306. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_detection_strategies.py +0 -0
  307. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_analyzer_context.py +0 -0
  308. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_divergence_integration.py +0 -0
  309. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_shape_integration.py +0 -0
  310. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_swing_integration.py +0 -0
  311. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_volatility_integration.py +0 -0
  312. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_volume_integration.py +0 -0
  313. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_models.py +0 -0
  314. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_pipeline.py +0 -0
  315. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_regression_analyzer.py +0 -0
  316. {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zones_analysis.py +0 -0
  317. {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_infrastructure.py +0 -0
  318. {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_swing_overlay.py +0 -0
  319. {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_zone_metrics_aggregation.py +0 -0
  320. {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_zone_metrics_display.py +0 -0
  321. {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_zones_visualization.py +0 -0
@@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.0.2] - 2026-07-20
13
+
14
+ ### Added
15
+ - **`SwingPoint.confirmation_index`** — маркер причинной доступности свинга: индекс
16
+ бара, к которому пивот (и его `amplitude_to_next`) причинно подтверждён. Позволяет
17
+ строить leak-free / look-ahead-free потребителей (устранение утечки J1). Реализован
18
+ для ZigZag-стратегии; прочие свинг-стратегии оставляют `None` (контракт допускает
19
+ мягкую деградацию). Сериализуется в `SwingContext.to_dict()`. (PR #107)
20
+
21
+ ### Changed
22
+ - **`CACHE_SCHEMA_VERSION` повышена до 2** — ключ дискового кэша зон-анализа теперь
23
+ учитывает версию схемы вывода, инвалидируя старые кэши без `confirmation_index`.
24
+ Инвариант: любое изменение схемы/семантики кэшируемого вывода обязано бампать
25
+ `CACHE_SCHEMA_VERSION`.
26
+
27
+ ## [0.0.1] - 2026-01-12
28
+
12
29
  ### Added
13
30
  - Initial release of BQuant package
14
31
  - Complete migration from Quanto project
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bquant
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Quantitative research toolkit for financial markets
5
5
  Author-email: kogriv <kogriv@gmail.com>
6
6
  License: MIT
@@ -59,6 +59,10 @@ Requires-Dist: jupyter==1.1.1; extra == "full"
59
59
  Requires-Dist: ipywidgets==8.1.7; extra == "full"
60
60
  Requires-Dist: plotly==6.3.0; extra == "full"
61
61
  Requires-Dist: kaleido==1.0.0; extra == "full"
62
+ Provides-Extra: research
63
+ Requires-Dist: pycatch22>=0.4.5; extra == "research"
64
+ Requires-Dist: pyts>=0.13.0; extra == "research"
65
+ Requires-Dist: tslearn>=0.9.0; extra == "research"
62
66
  Dynamic: license-file
63
67
 
64
68
  # BQuant - Quantitative Research Toolkit
@@ -5,7 +5,7 @@ A comprehensive toolkit for quantitative analysis of financial data,
5
5
  starting with MACD analysis and expandable to all aspects of market research.
6
6
  """
7
7
 
8
- __version__ = "0.0.1"
8
+ __version__ = "0.0.2"
9
9
  __author__ = "kogriv"
10
10
  __email__ = "kogriv@gmail.com"
11
11
 
@@ -43,6 +43,19 @@ class SwingPoint:
43
43
  duration_to_next: Number of bars to the next swing point (if available).
44
44
  strategy_name: Name of the strategy that detected the swing.
45
45
  strategy_params: Parameters of the strategy for traceability.
46
+ confirmation_index: Integer position of the bar by which this swing is
47
+ **causally confirmed** — i.e. the earliest bar at which a downstream,
48
+ leak-free consumer may treat the pivot (and therefore its
49
+ ``amplitude_to_next``) as known. Always ``>= index``. ``None`` means the
50
+ pivot is not yet confirmed within the available data (typically the last,
51
+ still-forming swing). The confirmation rule is **strategy-specific** (a
52
+ ZigZag pivot confirms once price retraces the configured deviation; a
53
+ fractal/pivot-point swing confirms after its fixed look-ahead window; etc.),
54
+ so each swing strategy populates this field per its own semantics. A strategy
55
+ that does not compute it leaves ``None``, and consumers must degrade
56
+ gracefully. This field exists to let forecasting/OOS code build prefixes
57
+ strictly from swings available at decision time, avoiding look-ahead through
58
+ ``amplitude_to_next`` (whose endpoint lies in the future of ``index``).
46
59
 
47
60
  Example:
48
61
  >>> from datetime import datetime
@@ -67,6 +80,7 @@ class SwingPoint:
67
80
  duration_to_next: Optional[int] = None
68
81
  strategy_name: str = ""
69
82
  strategy_params: Dict[str, Any] = None
83
+ confirmation_index: Optional[int] = None
70
84
 
71
85
  def __post_init__(self) -> None:
72
86
  if self.strategy_params is None:
@@ -162,6 +176,7 @@ class SwingContext:
162
176
  "duration_to_next": sp.duration_to_next,
163
177
  "strategy_name": sp.strategy_name,
164
178
  "strategy_params": sp.strategy_params,
179
+ "confirmation_index": sp.confirmation_index,
165
180
  }
166
181
  for sp in self.swing_points
167
182
  ],
@@ -43,6 +43,12 @@ _SWING_CLASS_TO_NAME = {
43
43
  PivotPointsSwingStrategy: "pivot_points",
44
44
  }
45
45
 
46
+ # Bump whenever the schema/semantics of cached analysis output changes (new
47
+ # SwingPoint/ZoneInfo fields, changed swing computation, etc.), so that on-disk
48
+ # caches computed under an older schema are not silently served.
49
+ # v2 (2026-07): SwingPoint.confirmation_index added (causal availability).
50
+ CACHE_SCHEMA_VERSION = 2
51
+
46
52
 
47
53
  @dataclass
48
54
  class IndicatorConfig:
@@ -66,7 +72,7 @@ class ZoneAnalysisConfig:
66
72
  n_clusters: int = 3
67
73
  run_regression: bool = False
68
74
  run_validation: bool = False
69
- swing_scope: Literal["per_zone", "global"] = "per_zone"
75
+ swing_scope: Literal["per_zone", "global"] = "global"
70
76
 
71
77
  def to_cache_key(self) -> str:
72
78
  """Serialize configuration into a stable JSON string for caching."""
@@ -89,6 +95,7 @@ class ZoneAnalysisConfig:
89
95
  "run_regression": self.run_regression,
90
96
  "run_validation": self.run_validation,
91
97
  "swing_scope": self.swing_scope,
98
+ "schema_version": CACHE_SCHEMA_VERSION,
92
99
  }
93
100
  return json.dumps(payload, sort_keys=True, default=str)
94
101
 
@@ -486,7 +493,7 @@ class ZoneAnalysisBuilder:
486
493
  result = (
487
494
  analyze_zones(df)
488
495
  .with_indicator('custom', 'macd', fast=12, slow=26, signal=9)
489
- .detect_zones('zero_crossing', indicator_col='macd_histogram')
496
+ .detect_zones('zero_crossing', indicator_col='macd')
490
497
  .analyze(clustering=True, n_clusters=3)
491
498
  .build()
492
499
  )
@@ -511,7 +518,7 @@ class ZoneAnalysisBuilder:
511
518
  self._volume_strategy: Optional[str] = None
512
519
  self._swing_preset: Optional[str] = None
513
520
  self._auto_swing_thresholds = False
514
- self._swing_scope: Literal["per_zone", "global"] = "per_zone"
521
+ self._swing_scope: Literal["per_zone", "global"] = "global"
515
522
  self.logger = get_logger(__name__)
516
523
 
517
524
  def with_indicator(self,
@@ -724,7 +731,7 @@ def analyze_zones(df: pd.DataFrame) -> ZoneAnalysisBuilder:
724
731
  # Минимальный пример
725
732
  result = (
726
733
  analyze_zones(df)
727
- .detect_zones('zero_crossing', indicator_col='macd_histogram')
734
+ .detect_zones('zero_crossing', indicator_col='macd')
728
735
  .build()
729
736
  )
730
737
 
@@ -732,7 +739,7 @@ def analyze_zones(df: pd.DataFrame) -> ZoneAnalysisBuilder:
732
739
  result = (
733
740
  analyze_zones(df)
734
741
  .with_indicator('custom', 'macd', fast=12, slow=26, signal=9)
735
- .detect_zones('zero_crossing', indicator_col='macd_histogram')
742
+ .detect_zones('zero_crossing', indicator_col='macd')
736
743
  .analyze(clustering=True, n_clusters=3, regression=True)
737
744
  .with_cache(enable=True, ttl=7200)
738
745
  .build()
@@ -40,6 +40,7 @@ def analyze_macd_zones(df: pd.DataFrame,
40
40
  min_duration: int = 2,
41
41
  zone_types: Optional[list] = None,
42
42
  smooth_window: Optional[int] = None,
43
+ zone_basis: str = 'line',
43
44
  clustering: bool = True,
44
45
  n_clusters: int = 3,
45
46
  regression: bool = False,
@@ -62,6 +63,11 @@ def analyze_macd_zones(df: pd.DataFrame,
62
63
  min_duration: Минимальная длительность зоны в барах (default: 2)
63
64
  zone_types: Типы зон для анализа (default: ['bull', 'bear'])
64
65
  smooth_window: Окно сглаживания для детекции (optional)
66
+ zone_basis: На чём детектировать зону (default: 'line'):
67
+ 'line' — по знаку линии MACD (MACD>0/<0), длинные стабильные зоны,
68
+ как в методологии проекта (indicator_col='macd');
69
+ 'histogram' — по знаку гистограммы (MACD−signal), более короткие
70
+ волатильные зоны (indicator_col='macd_hist', legacy).
65
71
  clustering: Выполнять кластеризацию (default: True)
66
72
  n_clusters: Количество кластеров (default: 3)
67
73
  regression: Запустить регрессионный анализ (default: False)
@@ -90,14 +96,21 @@ def analyze_macd_zones(df: pd.DataFrame,
90
96
  # Визуализация
91
97
  result.visualize('overview')
92
98
  """
99
+ _basis_to_col = {'line': 'macd', 'histogram': 'macd_hist'}
100
+ if zone_basis not in _basis_to_col:
101
+ raise ValueError(
102
+ f"zone_basis must be 'line' or 'histogram', got {zone_basis!r}"
103
+ )
104
+ indicator_col = _basis_to_col[zone_basis]
105
+
93
106
  builder = (
94
107
  analyze_zones(df)
95
- .with_indicator('custom', 'macd',
96
- fast_period=fast,
97
- slow_period=slow,
108
+ .with_indicator('custom', 'macd',
109
+ fast_period=fast,
110
+ slow_period=slow,
98
111
  signal_period=signal)
99
- .detect_zones('zero_crossing',
100
- indicator_col='macd_hist',
112
+ .detect_zones('zero_crossing',
113
+ indicator_col=indicator_col,
101
114
  min_duration=min_duration,
102
115
  zone_types=zone_types,
103
116
  smooth_window=smooth_window)
@@ -49,13 +49,14 @@ class ZigZagSwingStrategy:
49
49
  len(full_data),
50
50
  self.legs * 2,
51
51
  )
52
- return SwingContext(
53
- swing_points=[],
54
- indices=np.array([], dtype=int),
55
- full_data_length=len(full_data),
56
- strategy_name='zigzag',
57
- strategy_params={'legs': self.legs, 'deviation': self.deviation},
52
+ return self._empty_context(len(full_data))
53
+
54
+ if self._is_degenerate(full_data):
55
+ logger.warning(
56
+ "ZigZag global: degenerate input (near-constant high/low). "
57
+ "Skipping pandas-ta zigzag and returning empty SwingContext."
58
58
  )
59
+ return self._empty_context(len(full_data))
59
60
 
60
61
  from .....indicators import LibraryManager
61
62
 
@@ -71,13 +72,7 @@ class ZigZagSwingStrategy:
71
72
  logger.warning(
72
73
  "ZigZag returned insufficient columns, no swings detected"
73
74
  )
74
- return SwingContext(
75
- swing_points=[],
76
- indices=np.array([], dtype=int),
77
- full_data_length=len(full_data),
78
- strategy_name='zigzag',
79
- strategy_params={'legs': self.legs, 'deviation': self.deviation},
80
- )
75
+ return self._empty_context(len(full_data))
81
76
 
82
77
  swing_values = result.data.iloc[:, 1].dropna()
83
78
 
@@ -85,13 +80,7 @@ class ZigZagSwingStrategy:
85
80
  logger.warning(
86
81
  "ZigZag detected fewer than two swing points in global mode"
87
82
  )
88
- return SwingContext(
89
- swing_points=[],
90
- indices=np.array([], dtype=int),
91
- full_data_length=len(full_data),
92
- strategy_name='zigzag',
93
- strategy_params={'legs': self.legs, 'deviation': self.deviation},
94
- )
83
+ return self._empty_context(len(full_data))
95
84
 
96
85
  swing_points: List[SwingPoint] = []
97
86
  indices: List[int] = []
@@ -99,6 +88,9 @@ class ZigZagSwingStrategy:
99
88
  timestamps = swing_values.index.to_list()
100
89
  prices = swing_values.to_list()
101
90
 
91
+ high_arr = full_data['high'].to_numpy(dtype=float)
92
+ low_arr = full_data['low'].to_numpy(dtype=float)
93
+
102
94
  for point_id, (timestamp, price) in enumerate(zip(timestamps, prices)):
103
95
  ts = timestamp.to_pydatetime() if hasattr(timestamp, "to_pydatetime") else timestamp
104
96
  position_arr = full_data.index.get_indexer([timestamp])
@@ -118,6 +110,7 @@ class ZigZagSwingStrategy:
118
110
 
119
111
  amplitude_to_next: Optional[float] = None
120
112
  duration_to_next: Optional[int] = None
113
+ confirmation_index: Optional[int] = None
121
114
  if point_id < len(prices) - 1:
122
115
  next_price = prices[point_id + 1]
123
116
  next_timestamp = timestamps[point_id + 1]
@@ -127,6 +120,10 @@ class ZigZagSwingStrategy:
127
120
  if price != 0:
128
121
  amplitude_to_next = (next_price / price - 1) * 100
129
122
  duration_to_next = max(0, next_position - position)
123
+ confirmation_index = self._confirmation_index(
124
+ position, next_position, float(price), swing_type,
125
+ high_arr, low_arr,
126
+ )
130
127
 
131
128
  swing_points.append(
132
129
  SwingPoint(
@@ -139,6 +136,7 @@ class ZigZagSwingStrategy:
139
136
  duration_to_next=duration_to_next,
140
137
  strategy_name='zigzag',
141
138
  strategy_params={'legs': self.legs, 'deviation': self.deviation},
139
+ confirmation_index=confirmation_index,
142
140
  )
143
141
  )
144
142
  indices.append(position)
@@ -182,6 +180,13 @@ class ZigZagSwingStrategy:
182
180
  """
183
181
  self._validate_input(zone_data)
184
182
 
183
+ if self._is_degenerate(zone_data):
184
+ logger.debug(
185
+ "ZigZag: degenerate zone input (near-constant high/low), "
186
+ "returning empty metrics"
187
+ )
188
+ return self._empty_metrics()
189
+
185
190
  try:
186
191
  # Import LibraryManager dynamically to avoid circular imports
187
192
  from .....indicators import LibraryManager
@@ -413,9 +418,69 @@ class ZigZagSwingStrategy:
413
418
 
414
419
  return metrics
415
420
 
421
+ def _confirmation_index(
422
+ self,
423
+ position: int,
424
+ next_position: int,
425
+ price: float,
426
+ swing_type: str,
427
+ high_arr: np.ndarray,
428
+ low_arr: np.ndarray,
429
+ ) -> int:
430
+ """Bar by which this ZigZag pivot is causally confirmed.
431
+
432
+ A peak is confirmed at the first bar after ``position`` whose low has
433
+ fallen ``deviation`` below the pivot price; a trough, at the first bar
434
+ whose high has risen ``deviation`` above it. This is the ZigZag-specific
435
+ realisation of the generic :attr:`SwingPoint.confirmation_index` contract:
436
+ it is the point at which a repainting ZigZag "locks in" the previous swing,
437
+ i.e. when the next leg starts drawing. It is guaranteed to occur no later
438
+ than ``next_position`` (the next pivot already exceeds the deviation
439
+ threshold), which is used as a safe upper bound if no earlier crossing is
440
+ found. ``deviation`` is a fraction (0.05 == 5%), matching the pivot scale.
441
+ """
442
+ if next_position <= position:
443
+ return next_position
444
+ if swing_type == 'peak':
445
+ threshold = price * (1.0 - self.deviation)
446
+ seg = low_arr[position + 1:next_position + 1]
447
+ hits = np.nonzero(seg <= threshold)[0]
448
+ else:
449
+ threshold = price * (1.0 + self.deviation)
450
+ seg = high_arr[position + 1:next_position + 1]
451
+ hits = np.nonzero(seg >= threshold)[0]
452
+ if len(hits):
453
+ return position + 1 + int(hits[0])
454
+ return next_position
455
+
416
456
  def _empty_metrics(self) -> SwingMetrics:
417
457
  return self._aggregate_metrics([], [])
418
458
 
459
+ def _empty_context(self, full_data_length: int) -> SwingContext:
460
+ return SwingContext(
461
+ swing_points=[],
462
+ indices=np.array([], dtype=int),
463
+ full_data_length=full_data_length,
464
+ strategy_name='zigzag',
465
+ strategy_params={'legs': self.legs, 'deviation': self.deviation},
466
+ )
467
+
468
+ def _is_degenerate(self, data: pd.DataFrame) -> bool:
469
+ """
470
+ Detect input that makes pandas-ta's numba ZigZag segfault.
471
+
472
+ The nopython ZigZag kernel has no bounds checking and crashes (native
473
+ SIGSEGV/SIGABRT, uncatchable by try/except) when ``high`` or ``low`` is
474
+ (near-)constant, i.e. lacks enough distinct levels to form pivots over
475
+ ``legs``-bar windows. Real OHLC data has hundreds of distinct levels and
476
+ is never flagged; only degenerate/flat segments are. See
477
+ devref/gaps/zo/zo_issue_numba_linux_testsuite_2026-07.md.
478
+ """
479
+ return (
480
+ data['high'].nunique() < self.legs
481
+ or data['low'].nunique() < self.legs
482
+ )
483
+
419
484
  def _validate_input(self, data: pd.DataFrame) -> None:
420
485
  required_cols = {'high', 'low', 'close'}
421
486
  missing = required_cols.difference(data.columns)
@@ -0,0 +1,211 @@
1
+ """
2
+ Утилиты для визуализации BQuant.
3
+
4
+ Содержит вспомогательные функции для работы с временными рядами,
5
+ форматирования меток осей и других задач визуализации.
6
+ """
7
+ from typing import List, Tuple
8
+ import pandas as pd
9
+
10
+
11
+ def find_all_gaps(dt_index: pd.DatetimeIndex) -> list[list[str]]:
12
+ """
13
+ Анализирует DatetimeIndex, находит разрывы и возвращает их в формате,
14
+ совместимом с Plotly rangebreaks.
15
+ """
16
+ if not isinstance(dt_index, pd.DatetimeIndex) or len(dt_index) < 2:
17
+ return []
18
+
19
+ # 1. Вычисляем разницу во времени между соседними точками
20
+ time_diffs = dt_index.to_series().diff()
21
+
22
+ # 2. Определяем нормальный интервал (медиана - для устойчивости к выбросам)
23
+ # и порог для определения "разрыва" (например, в 1.5 раза больше нормы)
24
+ normal_interval = time_diffs.median()
25
+ gap_threshold = normal_interval * 1.5
26
+
27
+ # 3. Находим все точки, где начинается разрыв
28
+ gaps = time_diffs[time_diffs > gap_threshold]
29
+
30
+ if gaps.empty:
31
+ return []
32
+
33
+ # 4. Формируем список интервалов для Plotly
34
+ # Plotly ожидает список вида [ [start1, end1], [start2, end2], ... ]
35
+ # где start и end - строки в формате ISO (например, "2025-06-28T00:00:00")
36
+ rangebreak_values = []
37
+ for gap_end_time, duration in gaps.items():
38
+ gap_start_time = gap_end_time - duration
39
+
40
+ # Конвертируем Timestamp в строки ISO для Plotly
41
+ # Plotly rangebreaks требует строки в формате ISO
42
+ start_str = gap_start_time.isoformat() if hasattr(gap_start_time, 'isoformat') else str(gap_start_time)
43
+ end_str = gap_end_time.isoformat() if hasattr(gap_end_time, 'isoformat') else str(gap_end_time)
44
+
45
+ # Добавляем интервал в формате, который понимает Plotly
46
+ rangebreak_values.append([start_str, end_str])
47
+
48
+ return rangebreak_values
49
+
50
+
51
+ def generate_dense_axis_labels(
52
+ timestamps: List[pd.Timestamp],
53
+ positions: List[int],
54
+ num_ticks_requested: int = 16
55
+ ) -> Tuple[List[int], List[str]]:
56
+ """
57
+ Генерирует умные метки для оси X в режиме 'dense'.
58
+
59
+ Создает двухэтажные метки с адаптивным форматированием на основе
60
+ временного диапазона данных. Используется в визуализации зон для
61
+ единообразного отображения временных меток на графиках.
62
+
63
+ Логика форматирования:
64
+ - < 24 часа: только время (%H:%M)
65
+ - < 7 дней: дата и время (%d.%m + %H:%M) - двухэтажные метки
66
+ - < 30 дней: дата, время опционально (если все времена одинаковые - только дата)
67
+ - >= 30 дней: дата, год отдельно (%d.%m + жирный год) - двухэтажные метки
68
+
69
+ Args:
70
+ timestamps: Список временных меток (pd.Timestamp или совместимые типы).
71
+ Может быть получен из DataFrame.index
72
+ positions: Список позиционных индексов (0..N-1) для оси X.
73
+ Должен соответствовать timestamps по длине
74
+ num_ticks_requested: Желаемое количество меток (по умолчанию 16).
75
+ Автоматически корректируется на основе временного
76
+ диапазона для оптимальной читаемости
77
+
78
+ Returns:
79
+ Кортеж (tickvals, ticktext):
80
+ - tickvals: List[int] - позиции на оси X для меток
81
+ - ticktext: List[str] - текстовые метки (могут содержать HTML для двухэтажных меток)
82
+
83
+ Example:
84
+ >>> import pandas as pd
85
+ >>> from bquant.visualization.utils import generate_dense_axis_labels
86
+ >>>
87
+ >>> # Подготовка данных
88
+ >>> timestamps = pd.date_range('2025-01-01', periods=100, freq='H')
89
+ >>> positions = list(range(len(timestamps)))
90
+ >>>
91
+ >>> # Генерация меток
92
+ >>> tickvals, ticktext = generate_dense_axis_labels(timestamps, positions, num_ticks_requested=16)
93
+ >>>
94
+ >>> # Использование в Plotly
95
+ >>> fig.update_xaxes(tickmode='array', tickvals=tickvals, ticktext=ticktext)
96
+ """
97
+ # Проверяем входные данные (безопасная проверка для pandas объектов)
98
+ if not timestamps or len(timestamps) == 0:
99
+ return [], []
100
+ if not positions or len(positions) == 0:
101
+ return [], []
102
+
103
+ # Преобразуем timestamps в список pd.Timestamp для единообразия
104
+ # Обрабатываем разные типы входных данных (list, Index, Series)
105
+ if hasattr(timestamps, '__iter__') and not isinstance(timestamps, str):
106
+ x_dates = [pd.Timestamp(ts) if not isinstance(ts, pd.Timestamp) else ts for ts in timestamps]
107
+ else:
108
+ x_dates = [pd.Timestamp(timestamps)] if timestamps else []
109
+
110
+ # Определяем количество тиков (умная логика на основе временного диапазона)
111
+ data_points = len(positions)
112
+ if data_points > 0 and len(x_dates) > 0:
113
+ time_range = (x_dates[-1] - x_dates[0]).total_seconds()
114
+ if time_range < 3600 * 24:
115
+ # Меньше суток: больше тиков (30 точек на тик)
116
+ ideal_ticks = max(8, min(20, data_points // 30))
117
+ elif time_range < 3600 * 24 * 7:
118
+ # Меньше недели: средняя плотность (6 точек на тик)
119
+ ideal_ticks = max(8, min(20, data_points // 6))
120
+ elif time_range < 3600 * 24 * 30:
121
+ # Меньше месяца: меньше тиков (2 точки на тик)
122
+ ideal_ticks = max(8, min(20, data_points // 2))
123
+ else:
124
+ # Больше месяца: минимальная плотность (10 точек на тик)
125
+ ideal_ticks = max(8, min(20, data_points // 10))
126
+ num_ticks = max(8, min(num_ticks_requested, ideal_ticks, data_points))
127
+ else:
128
+ num_ticks = max(8, min(num_ticks_requested, len(positions)))
129
+
130
+ tick_step = max(1, len(positions) // num_ticks) if num_ticks > 0 else 1
131
+ tick_positions = positions[::tick_step]
132
+
133
+ # Умное форматирование даты/времени на основе временного диапазона
134
+ show_date, show_time, show_year_separately = True, True, False
135
+ if len(x_dates) > 0:
136
+ time_range = (x_dates[-1] - x_dates[0]).total_seconds()
137
+ if time_range < 3600 * 24:
138
+ # Меньше суток: только время
139
+ date_format, time_format, show_date = '%H:%M', '%H:%M', False
140
+ elif time_range < 3600 * 24 * 7:
141
+ # Меньше недели: дата и время
142
+ date_format, time_format = '%d.%m', '%H:%M'
143
+ elif time_range < 3600 * 24 * 30:
144
+ # Меньше месяца: дата, время опционально
145
+ date_format = '%d.%m'
146
+ unique_times = set(dt.strftime('%H:%M') for dt in x_dates if hasattr(dt, 'strftime'))
147
+ if len(unique_times) == 1:
148
+ # Все времена одинаковые - показываем только дату
149
+ time_format, show_time = '%d.%m', False
150
+ else:
151
+ time_format = '%H:%M'
152
+ else:
153
+ # Больше месяца: дата, год отдельно
154
+ date_format, time_format, show_time, show_year_separately = '%d.%m', '%d.%m', False, True
155
+ else:
156
+ date_format, time_format = '%d.%m', '%H:%M'
157
+
158
+ # Формируем двухэтажные метки
159
+ tick_labels = []
160
+ prev_year = None
161
+ for i in range(0, len(positions), tick_step):
162
+ if i < len(x_dates):
163
+ date_obj = x_dates[i]
164
+ current_year = date_obj.year if hasattr(date_obj, 'year') else None
165
+ show_year = show_year_separately and (prev_year is None or current_year != prev_year)
166
+
167
+ if hasattr(date_obj, 'strftime'):
168
+ date_str = date_obj.strftime(date_format)
169
+ time_str = date_obj.strftime(time_format)
170
+ else:
171
+ date_str = str(date_obj)
172
+ time_str = str(date_obj)
173
+
174
+ # Формируем метку с двухэтажным форматом (HTML для Plotly)
175
+ if show_year and current_year is not None:
176
+ label = f"{date_str}<br><b>{current_year}</b>"
177
+ elif show_date and show_time and date_str != time_str:
178
+ label = f"{date_str}<br>{time_str}"
179
+ elif show_date:
180
+ label = date_str
181
+ else:
182
+ label = time_str
183
+
184
+ tick_labels.append(label)
185
+ prev_year = current_year
186
+ else:
187
+ tick_labels.append('')
188
+
189
+ # Гарантируем наличие последнего тика для полноты отображения
190
+ if positions and positions[-1] not in tick_positions:
191
+ last_idx = len(positions) - 1
192
+ if last_idx < len(x_dates):
193
+ date_obj = x_dates[last_idx]
194
+ if hasattr(date_obj, 'strftime'):
195
+ date_str = date_obj.strftime(date_format)
196
+ time_str = date_obj.strftime(time_format)
197
+ else:
198
+ date_str = str(date_obj)
199
+ time_str = str(date_obj)
200
+
201
+ if show_date and show_time and date_str != time_str:
202
+ label = f"{date_str}<br>{time_str}"
203
+ elif show_date:
204
+ label = date_str
205
+ else:
206
+ label = time_str
207
+
208
+ tick_positions.append(positions[-1])
209
+ tick_labels.append(label)
210
+
211
+ return tick_positions, tick_labels