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.
- {bquant-0.0.1 → bquant-0.0.2}/CHANGELOG.md +17 -0
- {bquant-0.0.1/bquant.egg-info → bquant-0.0.2}/PKG-INFO +5 -1
- {bquant-0.0.1 → bquant-0.0.2}/bquant/__init__.py +1 -1
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/models.py +15 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/pipeline.py +12 -5
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/presets.py +18 -5
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/zigzag.py +85 -20
- bquant-0.0.2/bquant/visualization/utils.py +211 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/zones.py +153 -125
- {bquant-0.0.1 → bquant-0.0.2/bquant.egg-info}/PKG-INFO +5 -1
- {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/SOURCES.txt +6 -4
- {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/requires.txt +5 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/README.md +28 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/zones/pipeline.md.txt +3 -3
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/zone_analyzer_deep_dive.md.txt +103 -15
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/index.rst.txt +2 -1
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/migration/global_swings_migration.md.txt +13 -7
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/README.md.txt +6 -0
- bquant-0.0.2/docs/_build/html/_sources/user_guide/caching.md.txt +189 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/swing_strategies.md.txt +1 -1
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/zone_analysis.md.txt +84 -59
- bquant-0.0.2/docs/analytics/zones/layerA_ii_representations_case_study.md +68 -0
- bquant-0.0.2/docs/analytics/zones/layerA_zone_structure_case_study.md +73 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/analytics/zones/macd_zone_consistency_case_study.md +29 -0
- bquant-0.0.2/docs/analytics/zones/s1_swing_anatomy_case_study.md +127 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/zones/pipeline.md +3 -3
- {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/zone_analyzer_deep_dive.md +103 -15
- {bquant-0.0.1 → bquant-0.0.2}/docs/index.rst +2 -1
- {bquant-0.0.1 → bquant-0.0.2}/docs/migration/global_swings_migration.md +13 -7
- {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/README.md +12 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/best_practices.md +4 -1
- bquant-0.0.2/docs/user_guide/caching.md +189 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/swing_strategies.md +1 -1
- {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/zone_analysis.md +86 -59
- bquant-0.0.2/docs/user_guide/zone_analysis_result.md +442 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/04_comprehensive_analysis.py +1 -1
- {bquant-0.0.1 → bquant-0.0.2}/examples/08_macd_swing_analysis.py +1 -1
- {bquant-0.0.1 → bquant-0.0.2}/pyproject.toml +6 -1
- {bquant-0.0.1 → bquant-0.0.2}/scripts/analysis/batch_analysis.py +9 -1
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_strategy_infrastructure.py +22 -1
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_swing_global_calculation.py +22 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zigzag_swing_strategy.py +60 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_presets.py +2 -2
- bquant-0.0.1/bquant/indicators/base_old.py +0 -664
- bquant-0.0.1/bquant/indicators/library.py +0 -514
- bquant-0.0.1/bquant/visualization/utils.py +0 -41
- {bquant-0.0.1 → bquant-0.0.2}/LICENSE +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/MANIFEST.in +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/candlestick/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/chart/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/statistical/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/statistical/hypothesis_testing.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/statistical/regression.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/technical/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/timeseries/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/validation/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/validation/suite.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/analyzer.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/cache.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/base.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/combined.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/line_crossing.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/preloaded.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/registry.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/threshold.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/detection/zero_crossing.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/sequence_analysis.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/base.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/divergence/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/divergence/classic.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/registry.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/shape/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/shape/statistical.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/find_peaks.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/pivot_points.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/swing/thresholds.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/volatility/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/volatility/combined.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/volume/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/strategies/volume/standard.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/analysis/zones/zone_features.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/cli.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/cache.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/config.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/exceptions.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/logging_config.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/nb.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/numpy_fix.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/performance.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/core/utils.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/loader.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/processor.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/datasets.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/embedded/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/embedded/mt_xauusd_m15.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/embedded/tv_xauusd_1h.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/generator.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/samples/utils.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/schemas.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/data/validator.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/base.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/calculators.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/bollinger.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/ema.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/macd.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/rsi.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/custom/sma.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/library/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/library/manager.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/library/pandas_ta.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/library/talib.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/macd.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/preloaded/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/indicators/preloaded/macd.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/ml/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/charts.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/export.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/statistical.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant/visualization/themes.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/dependency_links.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/entry_points.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/bquant.egg-info/top_level.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/BUILD_ISSUES.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/analytics/zones/macd_zone_consistency_case_study.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/analytics/zones/swing.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/analytics/zones/swing_strategy_comparison_case_study.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/base.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/pipeline.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/statistical.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/strategies.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/zones/models.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/zones/strategies.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/analysis/zones.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/config.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/exceptions.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/logging.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/nb.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/performance.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/core/utils.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/loader.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/processor.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/samples.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/schemas.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/data/validator.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/extension_guide.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/base.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/factory.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/library_manager.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/macd.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/indicators/preloaded.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/visualization/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/api/visualization/zones.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/analytical_philosophy.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/statistical_analysis_workflow.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/developer_guide/zone_detection_strategies.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/examples/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/migration/MIGRATION_v2.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/README.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/combined_rules_detection.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/macd_basic_pipeline.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/preloaded_zones_workflow.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/tutorials/rsi_strategy_switching.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/best_practices.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/core_concepts.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/quick_start.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/_build/html/_sources/user_guide/swing_analysis_results.md.txt +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/analytics/zones/swing.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/analytics/zones/swing_strategy_comparison_case_study.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/base.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/pipeline.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/statistical.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/strategies.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/zones/models.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/zones/strategies.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/analysis/zones.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/config.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/exceptions.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/logging.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/nb.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/performance.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/core/utils.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/loader.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/processor.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/samples.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/schemas.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/data/validator.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/extension_guide.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/base.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/factory.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/library_manager.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/macd.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/indicators/preloaded.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/visualization/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/api/visualization/zones.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/analytical_philosophy.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/statistical_analysis_workflow.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/developer_guide/zone_detection_strategies.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/examples/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/migration/MIGRATION_v2.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/combined_rules_detection.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/macd_basic_pipeline.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/preloaded_zones_workflow.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/tutorials/rsi_strategy_switching.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/core_concepts.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/quick_start.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/docs/user_guide/swing_analysis_results.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/01_basic_indicators.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/02_macd_zone_analysis.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/02a_universal_zones.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/03_data_processing.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/05_strategies_demo.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/06_regression_demo.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/07_validation_demo.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/09_zones_visualization_demo.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/examples/zone_analysis_global_swings.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/analysis/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/analysis/run_macd_analysis.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/analysis/test_hypotheses.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/data/data_loader.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/data/extract_samples.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/data/generate_samples.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/data_processing/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/deployment/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/scripts/publishing/README.md +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/setup.cfg +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/analysis/zones/test_pipeline_cache.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/analysis/zones/test_swing_presets.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/analysis/zones/test_swing_thresholds.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/conftest.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/fixtures/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/fixtures/swing_mocks.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_backward_compatibility.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_data_pipeline.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_full_pipeline.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_pipeline_global_swings.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_scripts_integration.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_truly_universal_zones.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_visualization_pipeline.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/integration/test_zone_analysis_e2e.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/performance/test_swing_performance.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/__init__.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_all_indicator_types.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_analysis_structure.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_architecture_validation.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_builtin_migration.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_cache_versioning.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_classic_divergence_strategy.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_combined_volatility_strategy.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_compatibility_validation.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_core_modules.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_data_modules.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_divergence_strategy_universal.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_find_peaks_swing_strategy.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_imports_migration.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_indicator_factory.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_indicators_base.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_library_indicators.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_macd_analyzer.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_macd_backward_compatibility.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_pandas_ta_dynamic_loader.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_performance.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_pivot_points_swing_strategy.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_preloaded_indicators.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_sample_data.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_sample_generator.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_shape_strategy_universal.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_standard_volume_strategy.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_statistical_hypothesis.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_statistical_shape_strategy.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_swing_context_memory.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_swing_edge_cases.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_time_metrics.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_universal_zone_analyzer.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_validation_suite.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_visualization.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_volume_strategy_universal.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_analysis_builder.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_detection_strategies.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_analyzer_context.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_divergence_integration.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_shape_integration.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_swing_integration.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_volatility_integration.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_features_volume_integration.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_models.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_pipeline.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zone_regression_analyzer.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/unit/test_zones_analysis.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_infrastructure.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_swing_overlay.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_zone_metrics_aggregation.py +0 -0
- {bquant-0.0.1 → bquant-0.0.2}/tests/visualization/test_zone_metrics_display.py +0 -0
- {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.
|
|
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
|
|
@@ -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"] = "
|
|
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='
|
|
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"] = "
|
|
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='
|
|
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='
|
|
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=
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
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
|